Skip to content

Commit d4228fc

Browse files
committed
progress: showing the lineage graph
[ci skip]
1 parent 618474b commit d4228fc

9 files changed

Lines changed: 4078 additions & 6389 deletions

File tree

package-lock.json

Lines changed: 3893 additions & 6366 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vscode/extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"@types/fs-extra": "^11.0.4",
6161
"@vscode/python-extension": "^1.0.5",
6262
"fs-extra": "^11.3.0",
63-
"vscode-languageclient": "^9.0.1",
63+
"vscode-languageclient": "^9.0.1"
6464
},
6565
"devDependencies": {
6666
"@types/mocha": "^10.0.10",

vscode/react/src/components/graph/ModelLineage.tsx

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useApiModelLineage, useApiModels } from '@/api/index'
22
import { useEffect, useMemo, useRef, useState } from 'react'
3+
import { type ModelSQLMeshModel } from '@/domain/sqlmesh-model'
34
import { type HighlightedNodes, useLineageFlow } from './context'
45
import { ChevronDownIcon } from '@heroicons/react/24/solid'
56
import ReactFlow, {
@@ -18,6 +19,7 @@ import ReactFlow, {
1819
} from 'reactflow'
1920
import Loading from '@/components/loading/Loading'
2021
import Spinner from '@/components/logo/Spinner'
22+
import { createLineageWorker } from '@/workers'
2123
import { isArrayEmpty, isFalse, isNil, isNotNil } from '@/utils/index'
2224
import ListboxShow from '@/components/listbox/ListboxShow'
2325
import clsx from 'clsx'
@@ -42,7 +44,7 @@ export function ModelLineage({
4244
model,
4345
highlightedNodes,
4446
}: {
45-
model: Model
47+
model: ModelSQLMeshModel
4648
highlightedNodes?: HighlightedNodes
4749
}): JSX.Element {
4850
const {
@@ -67,24 +69,32 @@ export function ModelLineage({
6769
isFetching: isFetchingModelLineage,
6870
} = useApiModelLineage(model.name)
6971
const { isFetching: isFetchingModels } = useApiModels()
70-
const [isMergingModels, setIsMergingModels] = useState(false)
72+
73+
const [isMegringModels, setIsMergingModels] = useState(false)
7174
const [modelLineage, setModelLineage] =
7275
useState<ModelLineageApiLineageModelNameGet200 | undefined>(undefined)
7376

74-
console.log('model to lineage', model)
75-
console.log('modelLineage', modelLineage)
76-
console.log('models', models)
77-
console.log('isFetchingModelLineage', isFetchingModelLineage)
77+
useEffect(() => {
78+
const lineageWorker = createLineageWorker()
7879

80+
lineageWorker.addEventListener('message', handleLineageWorkerMessage)
7981

80-
useEffect(() => {
8182
getModelLineage()
8283
.then(({ data }) => {
8384
setModelLineage(data)
8485

8586
if (isNil(data)) return
8687

8788
setIsMergingModels(true)
89+
90+
lineageWorker.postMessage({
91+
topic: 'lineage',
92+
payload: {
93+
currentLineage: {},
94+
newLineage: data,
95+
mainNode: model.fqn,
96+
},
97+
})
8898
})
8999
.catch(error => {
90100
handleError?.(error)
@@ -101,6 +111,9 @@ export function ModelLineage({
101111
return () => {
102112
// void cancel?.()
103113

114+
lineageWorker.removeEventListener('message', handleLineageWorkerMessage)
115+
lineageWorker.terminate()
116+
104117
setLineage({})
105118
setNodeConnections({})
106119
setMainNode(undefined)
@@ -113,10 +126,10 @@ export function ModelLineage({
113126
modelName = encodeURI(modelName)
114127

115128
if (
116-
!models[modelName] &&
117-
!unknownModels[modelName]
129+
isFalse(modelName in models) &&
130+
isFalse(modelName in unknownModels)
118131
) {
119-
unknownModels[modelName]
132+
unknownModels.add(modelName)
120133
}
121134
})
122135

@@ -127,13 +140,27 @@ export function ModelLineage({
127140
setHighlightedNodes(highlightedNodes ?? {})
128141
}, [highlightedNodes])
129142

130-
const isFetching =
131-
isFetchingModelLineage || isFetchingModels || isMergingModels
132-
console.log('isFetchingModelLineage', isFetchingModelLineage)
133-
console.log('isFetchingModels', isFetchingModels)
134-
console.log('isMegringModels', isMergingModels)
135-
console.log('isFetching', isFetching)
143+
function handleLineageWorkerMessage(e: MessageEvent): void {
144+
if (e.data.topic === 'lineage') {
145+
setIsMergingModels(false)
146+
setNodeConnections(e.data.payload.nodesConnections)
147+
setLineage(e.data.payload.lineage)
136148

149+
if (
150+
Object.values(e.data.payload?.lineage ?? {}).length > WITH_COLUMNS_LIMIT
151+
) {
152+
setWithColumns(false)
153+
}
154+
}
155+
156+
if (e.data.topic === 'error') {
157+
handleError?.(e.data.error)
158+
setIsMergingModels(false)
159+
}
160+
}
161+
162+
const isFetching =
163+
isFetchingModelLineage || isFetchingModels || isMegringModels
137164

138165
return (
139166
<div className="relative h-full w-full overflow-hidden">

vscode/react/src/components/graph/ModelNode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default function ModelNode({
4646
} = useLineageFlow()
4747

4848
const columns: Column[] = useMemo(() => {
49-
const model = models.get(id)
49+
const model = models[id]
5050
const modelColumns = model?.columns ?? []
5151

5252
Object.keys(lineage[id]?.columns ?? {}).forEach((column: string) => {

vscode/react/src/components/graph/help.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ function getNodeMap({
172172
const modelNames = Object.keys(lineage)
173173

174174
return modelNames.reduce((acc: Record<string, Node>, modelName: string) => {
175-
const model = models.get(modelName)
175+
const model = models[modelName]
176176
const node = createGraphNode(modelName, {
177177
label: model?.displayName ?? modelName,
178178
withColumns,
@@ -187,7 +187,7 @@ function getNodeMap({
187187
: EnumLineageNodeModelType.cte,
188188
})
189189
const columnsCount = withColumns
190-
? (models.get(modelName)?.columns?.length ?? 0)
190+
? (models[modelName]?.columns?.length ?? 0)
191191
: 0
192192

193193
const maxWidth = Math.min(
@@ -216,7 +216,7 @@ function getNodeMap({
216216

217217
function getNodeMaxWidth(label: string, hasColumns: boolean = false): number {
218218
const defaultWidth = label.length * CHAR_WIDTH
219-
const columns = models.get(label)?.columns ?? []
219+
const columns = models[label]?.columns ?? []
220220

221221
return hasColumns
222222
? Math.max(...columns.map(getColumnWidth), defaultWidth)

vscode/react/src/domain/lineage.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type LineageColumn } from '@/api/client'
2+
3+
export interface Lineage {
4+
models: string[]
5+
columns?: Record<string, LineageColumn>
6+
}

vscode/react/src/utils/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function isNumber(value: unknown): value is number {
1818
return typeof value === 'number'
1919
}
2020

21-
export function isPrimitive(value: unknown): value is Primitive {
21+
export function isPrimitive(value: unknown): value is string | number | boolean {
2222
return isString(value) || isNumber(value) || typeof value === 'boolean'
2323
}
2424

@@ -130,7 +130,7 @@ export function toRatio(
130130
return (top / bottom) * multiplier
131131
}
132132

133-
export function parseJSON<T>(value: string | null): Optional<T> {
133+
export function parseJSON<T>(value: string | null): T | undefined {
134134
if (isNil(value)) return undefined
135135

136136
try {

vscode/react/src/workers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import createLineageWorker from './lineage.ts?worker&inline'
2+
3+
export { createLineageWorker }
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { isFalse, isNil, isStringEmptyOrNil, toID } from '@/utils/index'
2+
import { type Lineage } from '@/domain/lineage'
3+
4+
export interface ConnectedNode {
5+
id?: string
6+
edges: ConnectedNode[]
7+
}
8+
9+
const EnumDirection = {
10+
Upstream: 'upstream',
11+
Downstream: 'downstream',
12+
} as const
13+
14+
type Direction = (typeof EnumDirection)[keyof typeof EnumDirection]
15+
16+
const scope = self as any
17+
18+
scope.onmessage = async (e: MessageEvent) => {
19+
if (e.data.topic === 'lineage') {
20+
try {
21+
const { currentLineage, newLineage, mainNode } = e.data.payload
22+
const lineage = await mergeLineageWithModels(currentLineage, newLineage)
23+
const nodesConnections = await getNodesConnections(mainNode, lineage)
24+
25+
scope.postMessage({
26+
topic: 'lineage',
27+
payload: {
28+
lineage,
29+
nodesConnections,
30+
},
31+
})
32+
} catch (error) {
33+
scope.postMessage({
34+
topic: 'error',
35+
error,
36+
})
37+
}
38+
}
39+
}
40+
41+
async function mergeLineageWithModels(
42+
currentLineage: Record<string, Lineage> = {},
43+
data: Record<string, string[]> = {},
44+
): Promise<Record<string, Lineage>> {
45+
return Object.entries(data).reduce(
46+
(acc: Record<string, Lineage>, [key, models = []]) => {
47+
key = encodeURI(key)
48+
49+
acc[key] = {
50+
models: models.map(encodeURI),
51+
columns: currentLineage?.[key]?.columns ?? undefined,
52+
}
53+
54+
return acc
55+
},
56+
{},
57+
)
58+
}
59+
60+
async function getNodesConnections(
61+
mainNode: string,
62+
lineage: Record<string, Lineage> = {},
63+
): Promise<Record<string, ConnectedNode>> {
64+
return new Promise((resolve, reject) => {
65+
if (isNil(lineage) || isNil(mainNode)) return {}
66+
67+
const distances: Record<string, ConnectedNode> = {}
68+
69+
try {
70+
getConnectedNodes(EnumDirection.Upstream, mainNode, lineage, distances)
71+
getConnectedNodes(EnumDirection.Downstream, mainNode, lineage, distances)
72+
} catch (error) {
73+
reject(error)
74+
}
75+
76+
resolve(distances)
77+
})
78+
}
79+
80+
function getConnectedNodes(
81+
direction: Direction = EnumDirection.Downstream,
82+
node: string,
83+
lineage: Record<string, Lineage> = {},
84+
result: Record<string, ConnectedNode> = {},
85+
): void {
86+
const isDownstream = direction === EnumDirection.Downstream
87+
let models: string[] = []
88+
89+
if (isDownstream) {
90+
models = Object.keys(lineage).filter(key =>
91+
lineage[key]!.models.includes(node),
92+
)
93+
} else {
94+
models = lineage[node]?.models ?? []
95+
}
96+
97+
if (isFalse(node in result)) {
98+
result[node] = createConnectedNode()
99+
}
100+
101+
for (const model of models) {
102+
const connectedNode = isDownstream
103+
? createConnectedNode(node, model, [result[node]!])
104+
: createConnectedNode(model, node, [result[node]!])
105+
106+
if (model in result) {
107+
result[model]!.edges.push(connectedNode)
108+
} else {
109+
result[model] = connectedNode
110+
getConnectedNodes(direction, model, lineage, result)
111+
}
112+
}
113+
}
114+
115+
function createConnectedNode(
116+
source?: string,
117+
target?: string,
118+
edges: ConnectedNode[] = [],
119+
): ConnectedNode {
120+
const id = toID(source, target)
121+
122+
return {
123+
id: isStringEmptyOrNil(id) ? undefined : id,
124+
edges,
125+
}
126+
}

0 commit comments

Comments
 (0)