From 5a37f584aa9ac1734147357854aac444069202f3 Mon Sep 17 00:00:00 2001 From: SuLea-IT <105108570+SuLea-IT@users.noreply.github.com> Date: Sat, 2 May 2026 21:20:20 +0800 Subject: [PATCH 1/2] feat: add field lineage viewer --- src/App.vue | 27 ++ src/components/lineage/FieldLineageDialog.vue | 255 ++++++++++++++++++ src/components/sidebar/TreeItem.vue | 26 +- src/i18n/locales/en.ts | 11 + src/i18n/locales/zh-CN.ts | 11 + src/lib/fieldLineage.ts | 216 +++++++++++++++ src/stores/connectionStore.ts | 3 + tests/fieldLineage.test.ts | 76 ++++++ 8 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 src/components/lineage/FieldLineageDialog.vue create mode 100644 src/lib/fieldLineage.ts create mode 100644 tests/fieldLineage.test.ts diff --git a/src/App.vue b/src/App.vue index 5acfff5fb..51169aa85 100644 --- a/src/App.vue +++ b/src/App.vue @@ -41,6 +41,7 @@ const SchemaDiagramDialog = defineAsyncComponent(() => import("@/components/diag const TableImportDialog = defineAsyncComponent(() => import("@/components/import/TableImportDialog.vue")); const TableStructureEditorDialog = defineAsyncComponent(() => import("@/components/structure/TableStructureEditorDialog.vue")); const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue")); +const FieldLineageDialog = defineAsyncComponent(() => import("@/components/lineage/FieldLineageDialog.vue")); import type { ConnectionConfig } from "@/types/database"; import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; @@ -128,6 +129,7 @@ const showSqlFileDialog = ref(false); const showDiagramDialog = ref(false); const showTableImportDialog = ref(false); const showStructureEditorDialog = ref(false); +const showFieldLineageDialog = ref(false); const transferPrefillConnectionId = ref(""); const transferPrefillDatabase = ref(""); const schemaDiffPrefillConnectionId = ref(""); @@ -146,6 +148,11 @@ const structurePrefillConnectionId = ref(""); const structurePrefillDatabase = ref(""); const structurePrefillSchema = ref(""); const structurePrefillTable = ref(""); +const lineagePrefillConnectionId = ref(""); +const lineagePrefillDatabase = ref(""); +const lineagePrefillSchema = ref(""); +const lineagePrefillTable = ref(""); +const lineagePrefillColumn = ref(""); const databaseOptions = ref>({}); const loadingDatabaseOptions = ref>({}); const checkingUpdates = ref(false); @@ -233,6 +240,18 @@ watch(() => connectionStore.structureEditorSource, (v) => { } }); +watch(() => connectionStore.fieldLineageSource, (v) => { + if (v) { + lineagePrefillConnectionId.value = v.connectionId; + lineagePrefillDatabase.value = v.database; + lineagePrefillSchema.value = v.schema ?? ""; + lineagePrefillTable.value = v.tableName; + lineagePrefillColumn.value = v.columnName; + showFieldLineageDialog.value = true; + connectionStore.fieldLineageSource = null; + } +}); + async function onStructureEditorSaved() { const tab = activeTab.value; if (tab?.mode === "data" && tab.tableMeta?.tableName === structurePrefillTable.value) { @@ -1411,6 +1430,14 @@ async function setupFileDrop() { :prefill-table="structurePrefillTable" @saved="onStructureEditorSaved" /> + diff --git a/src/components/lineage/FieldLineageDialog.vue b/src/components/lineage/FieldLineageDialog.vue new file mode 100644 index 000000000..10648af96 --- /dev/null +++ b/src/components/lineage/FieldLineageDialog.vue @@ -0,0 +1,255 @@ + + + diff --git a/src/components/sidebar/TreeItem.vue b/src/components/sidebar/TreeItem.vue index 2c9428863..1440ac8a1 100644 --- a/src/components/sidebar/TreeItem.vue +++ b/src/components/sidebar/TreeItem.vue @@ -45,6 +45,7 @@ const sqlFileUnsupportedTypes = new Set(["redis", "mongodb", "elasticsearch"]); const diagramSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]); const tableImportSupportedTypes = new Set(["mysql", "postgres", "sqlite", "duckdb", "clickhouse", "sqlserver", "oracle", "doris", "starrocks", "redshift"]); const tableStructureSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver"]); +const fieldLineageSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]); const isExportingDatabase = ref(false); function currentDatabaseType(): DatabaseType | undefined { @@ -537,6 +538,19 @@ function openStructureEditor() { }; } +function openFieldLineage() { + const node = props.node; + const column = node.type === "column" && node.meta && "name" in node.meta ? node.meta.name : node.label; + if (node.type !== "column" || !node.connectionId || !node.database || !node.tableName || !column) return; + connectionStore.fieldLineageSource = { + connectionId: node.connectionId, + database: node.database, + schema: node.schema, + tableName: node.tableName, + columnName: column, + }; +} + const canExpand = !leafTypes.has(props.node.type); const canPin = computed(() => pinnableTypes.has(props.node.type)); const canOpenSqlFileExecution = computed(() => { @@ -555,10 +569,14 @@ const canOpenStructureEditor = computed(() => { const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined; return props.node.type === "table" && !!props.node.database && !!config && tableStructureSupportedTypes.has(config.db_type); }); +const canOpenFieldLineage = computed(() => { + const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined; + return props.node.type === "column" && !!props.node.database && !!props.node.tableName && !!config && fieldLineageSupportedTypes.has(config.db_type); +}); const isPinned = computed(() => props.node.pinned || connectionStore.isTreeNodePinned(props.node.id)); const hasTypeMenu = computed(() => { const t = props.node.type; - return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || isGroupLabel(props.node); + return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || t === "column" || isGroupLabel(props.node); }); const columnComment = computed(() => props.node.type === "column" && props.node.meta && "comment" in props.node.meta ? (props.node.meta as any).comment : null); const paddingLeft = `${props.depth * 16 + 8}px`; @@ -743,6 +761,12 @@ async function showMore() { + +