|
|
@@ -0,0 +1,137 @@
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <div class="d-flex justify-end mb-3">
|
|
|
+ <v-btn outlined color="primary" :loading="loading" @click="loadAll">刷新</v-btn>
|
|
|
+ </div>
|
|
|
+ <v-chip v-if="canManage" small color="primary" outlined class="mb-3">具备连接器管理权限</v-chip>
|
|
|
+ <v-row>
|
|
|
+ <v-col v-for="item in manifests" :key="`${item.connector_id}:${item.version}`" cols="12" md="4">
|
|
|
+ <v-card outlined height="100%">
|
|
|
+ <v-card-title>{{ item.display_name }}</v-card-title>
|
|
|
+ <v-card-subtitle>{{ item.connector_id }} · {{ item.version }} · SDK {{ item.sdk_version }}</v-card-subtitle>
|
|
|
+ <v-card-text>
|
|
|
+ <v-chip v-for="capability in item.capabilities" :key="capability" small outlined class="mr-1 mb-1">{{ capability }}</v-chip>
|
|
|
+ <div class="mt-3">兼容状态:<strong>{{ compatibility[`${item.connector_id}:${item.version}`] || '待检查' }}</strong></div>
|
|
|
+ </v-card-text>
|
|
|
+ <v-card-actions>
|
|
|
+ <v-btn text color="primary" @click="checkCompatibility(item)">兼容检查</v-btn>
|
|
|
+ <v-btn v-if="canOperate" text color="primary" @click="openDryRun(item)">Dry-run</v-btn>
|
|
|
+ </v-card-actions>
|
|
|
+ </v-card>
|
|
|
+ </v-col>
|
|
|
+ </v-row>
|
|
|
+ <v-card outlined class="mt-5">
|
|
|
+ <v-card-title>运行与检查点</v-card-title>
|
|
|
+ <v-data-table :headers="runHeaders" :items="runs" :loading="loading">
|
|
|
+ <template v-slot:[`item.error_category`]="{ item }">{{ item.error_category || '-' }}</template>
|
|
|
+ <template v-slot:[`item.run_type`]="{ item }">{{ isHumanDryRun(item) ? '人工 Dry-run' : '机器运行' }}</template>
|
|
|
+ <template v-slot:[`item.checkpoint_summary`]="{ item }"><code>{{ compact(item.checkpoint_summary) }}</code></template>
|
|
|
+ <template v-slot:[`item.cursor_summary`]="{ item }"><code>{{ compact(item.cursor_summary) }}</code></template>
|
|
|
+ <template v-slot:[`item.actions`]="{ item }">
|
|
|
+ <v-btn v-if="canOperate && isHumanDryRun(item) && item.status === 'running'" text small color="warning" @click="cancel(item)">取消</v-btn>
|
|
|
+ <v-btn v-if="canOperate && isHumanDryRun(item) && ['failed','cancelled'].includes(item.status)" text small color="primary" @click="resume(item)">恢复</v-btn>
|
|
|
+ <span v-if="!isHumanDryRun(item)" class="text--secondary">仅机器凭证可操作</span>
|
|
|
+ </template>
|
|
|
+ </v-data-table>
|
|
|
+ </v-card>
|
|
|
+ <v-card outlined class="mt-5">
|
|
|
+ <v-card-title>数据关系摘要</v-card-title>
|
|
|
+ <v-card-text>节点 {{ graph.node_count || 0 }} · 关系 {{ graph.edge_count || 0 }}</v-card-text>
|
|
|
+ </v-card>
|
|
|
+ <v-dialog v-model="dialog" max-width="620">
|
|
|
+ <v-card>
|
|
|
+ <v-card-title>连接器 Dry-run</v-card-title>
|
|
|
+ <v-card-text>
|
|
|
+ <v-text-field v-model.trim="form.source_uid" label="数据源 UID *" />
|
|
|
+ <v-text-field v-model.trim="form.credential_ref" label="秘密引用 *" hint="例如 env:DATAOPS_CONNECTOR_CREDENTIAL" persistent-hint />
|
|
|
+ <v-text-field v-if="mode === 'rest-catalog'" v-model.trim="form.base_url" label="HTTPS Catalog URL *" />
|
|
|
+ <v-text-field v-if="mode === 'rest-catalog'" v-model.trim="form.allowed_host" label="允许主机 *" />
|
|
|
+ </v-card-text>
|
|
|
+ <v-card-actions><v-spacer /><v-btn text @click="dialog=false">取消</v-btn><v-btn color="primary" @click="dryRun">执行</v-btn></v-card-actions>
|
|
|
+ </v-card>
|
|
|
+ </v-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { cancelConnectorRun, executeConnectorRun, getConnectorCompatibility, getConnectorManifests, getConnectorRuns, getDatasourceGraph, resumeConnectorRun } from '@/api/dataOrigin'
|
|
|
+
|
|
|
+const CONNECTOR_ID_PATTERN = /^[a-z][a-z0-9_-]{2,63}$/
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'ConnectorOperations',
|
|
|
+ props: {
|
|
|
+ connectorIds: {
|
|
|
+ type: Array,
|
|
|
+ required: true,
|
|
|
+ validator: value => Array.isArray(value) && value.length > 0 && value.every(id => typeof id === 'string' && CONNECTOR_ID_PATTERN.test(id))
|
|
|
+ },
|
|
|
+ mode: {
|
|
|
+ type: String,
|
|
|
+ required: true,
|
|
|
+ validator: value => ['database', 'rest-catalog'].includes(value)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data: () => ({
|
|
|
+ loading: false,
|
|
|
+ manifests: [],
|
|
|
+ runs: [],
|
|
|
+ compatibility: {},
|
|
|
+ graph: {},
|
|
|
+ dialog: false,
|
|
|
+ selected: null,
|
|
|
+ form: { source_uid: '', credential_ref: 'env:DATAOPS_CONNECTOR_CREDENTIAL', base_url: '', allowed_host: '' },
|
|
|
+ runHeaders: [
|
|
|
+ { text: '连接器', value: 'connector_id' }, { text: '运行类型', value: 'run_type' }, { text: '操作', value: 'operation' }, { text: '状态', value: 'status' },
|
|
|
+ { text: '尝试', value: 'attempt_count' }, { text: '检查点摘要', value: 'checkpoint_summary' }, { text: '游标摘要', value: 'cursor_summary' }, { text: '错误类别', value: 'error_category' }, { text: '操作', value: 'actions', sortable: false }
|
|
|
+ ]
|
|
|
+ }),
|
|
|
+ computed: {
|
|
|
+ connectorPermissions () { return (this.$store.state.user.userInfo && this.$store.state.user.userInfo.permissions) || [] },
|
|
|
+ canRead () { return this.connectorPermissions.includes('connectors:read') },
|
|
|
+ canOperate () { return this.connectorPermissions.includes('connectors:operate') || this.canManage },
|
|
|
+ canManage () { return this.connectorPermissions.includes('connectors:manage') },
|
|
|
+ normalizedConnectorIds () {
|
|
|
+ if (!Array.isArray(this.connectorIds) || this.connectorIds.some(id => typeof id !== 'string' || !CONNECTOR_ID_PATTERN.test(id))) return []
|
|
|
+ return [...new Set(this.connectorIds)]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created () { this.loadAll() },
|
|
|
+ methods: {
|
|
|
+ compact (value) { const text = JSON.stringify(value || {}); return text.length > 100 ? `${text.slice(0, 100)}…` : text },
|
|
|
+ isHumanDryRun (item) { return item.dry_run === true && !item.principal_uid },
|
|
|
+ clearConnectorState () { this.manifests = []; this.runs = []; this.graph = {} },
|
|
|
+ async loadAll () {
|
|
|
+ this.clearConnectorState()
|
|
|
+ if (!this.normalizedConnectorIds.length) return
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const allowedConnectorIds = new Set(this.normalizedConnectorIds)
|
|
|
+ const [manifests, runs, graph] = await Promise.all([getConnectorManifests(), getConnectorRuns(), getDatasourceGraph()])
|
|
|
+ this.manifests = (manifests.data.manifests || []).filter(item => allowedConnectorIds.has(item.connector_id))
|
|
|
+ this.runs = (runs.data.runs || []).filter(item => allowedConnectorIds.has(item.connector_id))
|
|
|
+ this.graph = graph.data.summary || {}
|
|
|
+ } catch (error) {
|
|
|
+ this.clearConnectorState()
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async checkCompatibility (item) { try { const { data } = await getConnectorCompatibility(item.connector_id, item.version); this.$set(this.compatibility, `${item.connector_id}:${item.version}`, data.compatible ? '兼容' : '不兼容') } catch (error) { this.$snackbar.error(error) } },
|
|
|
+ openDryRun (item) { this.selected = item; this.dialog = true },
|
|
|
+ async dryRun () {
|
|
|
+ if (!this.selected) return
|
|
|
+ const config = { credential_ref: this.form.credential_ref }
|
|
|
+ if (this.mode === 'rest-catalog') Object.assign(config, { base_url: this.form.base_url, allowed_host: this.form.allowed_host })
|
|
|
+ try {
|
|
|
+ await executeConnectorRun({ connector_id: this.selected.connector_id, version: this.selected.version, source_uid: this.form.source_uid, operation: 'discover', config, scope: {}, dry_run: true })
|
|
|
+ this.dialog = false
|
|
|
+ await this.loadAll()
|
|
|
+ } catch (error) { this.$snackbar.error(error) }
|
|
|
+ },
|
|
|
+ async cancel (item) { try { await cancelConnectorRun(item.idempotency_key); await this.loadAll() } catch (error) { this.$snackbar.error(error) } },
|
|
|
+ async resume (item) { try { await resumeConnectorRun(item.idempotency_key); await this.loadAll() } catch (error) { this.$snackbar.error(error) } }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|