@finos/legend-query-builder 4.17.9 → 4.17.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/lineage/LineageViewer.d.ts +22 -0
- package/lib/components/lineage/LineageViewer.d.ts.map +1 -0
- package/lib/components/lineage/LineageViewer.js +381 -0
- package/lib/components/lineage/LineageViewer.js.map +1 -0
- package/lib/index.css +1 -1
- package/lib/index.css.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/package.json +2 -1
- package/lib/stores/lineage/LineageState.d.ts +33 -0
- package/lib/stores/lineage/LineageState.d.ts.map +1 -0
- package/lib/stores/lineage/LineageState.js +49 -0
- package/lib/stores/lineage/LineageState.js.map +1 -0
- package/package.json +6 -5
- package/src/components/lineage/LineageViewer.tsx +561 -0
- package/src/index.ts +3 -0
- package/src/stores/lineage/LineageState.ts +56 -0
- package/tsconfig.json +2 -0
@@ -0,0 +1,561 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright (c) 2020-present, Goldman Sachs
|
3
|
+
*
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
* you may not use this file except in compliance with the License.
|
6
|
+
* You may obtain a copy of the License at
|
7
|
+
*
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
*
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
* See the License for the specific language governing permissions and
|
14
|
+
* limitations under the License.
|
15
|
+
*/
|
16
|
+
|
17
|
+
import { useEffect } from 'react';
|
18
|
+
import {
|
19
|
+
PanelContent,
|
20
|
+
clsx,
|
21
|
+
Dialog,
|
22
|
+
Modal,
|
23
|
+
ModalHeader,
|
24
|
+
ModalBody,
|
25
|
+
ModalFooter,
|
26
|
+
ModalFooterButton,
|
27
|
+
} from '@finos/legend-art';
|
28
|
+
import { observer } from 'mobx-react-lite';
|
29
|
+
import {
|
30
|
+
ReactFlow,
|
31
|
+
Background,
|
32
|
+
Controls,
|
33
|
+
MiniMap,
|
34
|
+
ReactFlowProvider,
|
35
|
+
Position,
|
36
|
+
type Node as ReactFlowNode,
|
37
|
+
type Edge as ReactFlowEdge,
|
38
|
+
} from 'reactflow';
|
39
|
+
import {
|
40
|
+
type LineageState,
|
41
|
+
LINEAGE_VIEW_MODE,
|
42
|
+
} from '../../stores/lineage/LineageState.js';
|
43
|
+
|
44
|
+
import {
|
45
|
+
type Graph,
|
46
|
+
type Owner,
|
47
|
+
type ReportLineage,
|
48
|
+
type LineageNode,
|
49
|
+
type LineageEdge,
|
50
|
+
} from '@finos/legend-graph';
|
51
|
+
|
52
|
+
function autoLayoutNodesAndEdges<T extends { id: string }>(
|
53
|
+
nodes: T[],
|
54
|
+
edges: { source: string; target: string }[],
|
55
|
+
xSpacing = 220,
|
56
|
+
ySpacing = 120,
|
57
|
+
areaHeight = 800,
|
58
|
+
): Record<string, { x: number; y: number }> {
|
59
|
+
// Build in-degree map
|
60
|
+
const nodeIds = nodes.map((n) => n.id);
|
61
|
+
const inDegree: Record<string, number> = {};
|
62
|
+
nodeIds.forEach((id) => {
|
63
|
+
inDegree[id] = 0;
|
64
|
+
});
|
65
|
+
edges.forEach((e) => {
|
66
|
+
inDegree[e.target] = (inDegree[e.target] ?? 0) + 1;
|
67
|
+
});
|
68
|
+
|
69
|
+
// BFS to assign levels
|
70
|
+
const levels: Record<string, number> = {};
|
71
|
+
const queue: string[] = [];
|
72
|
+
|
73
|
+
nodeIds.forEach((id) => {
|
74
|
+
if (inDegree[id] === 0) {
|
75
|
+
levels[id] = 0;
|
76
|
+
queue.push(id);
|
77
|
+
}
|
78
|
+
});
|
79
|
+
|
80
|
+
while (queue.length > 0) {
|
81
|
+
const current = queue.shift();
|
82
|
+
if (current === undefined) {
|
83
|
+
continue; // should never happen, but safe fallback
|
84
|
+
}
|
85
|
+
|
86
|
+
const currentLevel = levels[current] ?? 0;
|
87
|
+
edges.forEach((e) => {
|
88
|
+
if (e.source === current) {
|
89
|
+
const targetLevel = levels[e.target] ?? -1;
|
90
|
+
if (targetLevel < currentLevel + 1) {
|
91
|
+
levels[e.target] = currentLevel + 1;
|
92
|
+
queue.push(e.target);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
});
|
96
|
+
}
|
97
|
+
|
98
|
+
// Group nodes by level
|
99
|
+
const levelNodes: Record<number, string[]> = {};
|
100
|
+
Object.entries(levels).forEach(([id, lvl]) => {
|
101
|
+
if (!levelNodes[lvl]) {
|
102
|
+
levelNodes[lvl] = [];
|
103
|
+
}
|
104
|
+
levelNodes[lvl].push(id);
|
105
|
+
});
|
106
|
+
|
107
|
+
// Position nodes
|
108
|
+
const positions: Record<string, { x: number; y: number }> = {};
|
109
|
+
const maxLevel = Object.values(levels).length
|
110
|
+
? Math.max(...Object.values(levels))
|
111
|
+
: 0;
|
112
|
+
|
113
|
+
for (let lvl = 0; lvl <= maxLevel; lvl++) {
|
114
|
+
const ids = levelNodes[lvl] ?? [];
|
115
|
+
const totalHeight = (ids.length - 1) * ySpacing;
|
116
|
+
const startY = (areaHeight - totalHeight) / 2;
|
117
|
+
ids.forEach((id, idx) => {
|
118
|
+
positions[id] = {
|
119
|
+
x: 80 + lvl * xSpacing,
|
120
|
+
y: startY + idx * ySpacing,
|
121
|
+
};
|
122
|
+
});
|
123
|
+
}
|
124
|
+
|
125
|
+
// Fallback for disconnected nodes
|
126
|
+
nodeIds.forEach((id, idx) => {
|
127
|
+
if (!positions[id]) {
|
128
|
+
positions[id] = {
|
129
|
+
x: 80,
|
130
|
+
y: 80 + idx * ySpacing,
|
131
|
+
};
|
132
|
+
}
|
133
|
+
});
|
134
|
+
|
135
|
+
return positions;
|
136
|
+
}
|
137
|
+
|
138
|
+
function getLayoutBounds(positions: Record<string, { x: number; y: number }>) {
|
139
|
+
const xs = Object.values(positions).map((p) => p.x);
|
140
|
+
const ys = Object.values(positions).map((p) => p.y);
|
141
|
+
if (!xs.length || !ys.length) {
|
142
|
+
return { width: 800, height: 600 };
|
143
|
+
}
|
144
|
+
const minX = Math.min(...xs);
|
145
|
+
const maxX = Math.max(...xs);
|
146
|
+
const minY = Math.min(...ys);
|
147
|
+
const maxY = Math.max(...ys);
|
148
|
+
// Add some padding
|
149
|
+
return {
|
150
|
+
width: Math.max(400, maxX - minX + 200),
|
151
|
+
height: Math.max(300, maxY - minY + 200),
|
152
|
+
};
|
153
|
+
}
|
154
|
+
|
155
|
+
const convertGraphToFlow = (graph?: Graph) => {
|
156
|
+
if (!graph?.nodes.length) {
|
157
|
+
// Handle missing or empty graph
|
158
|
+
return {
|
159
|
+
nodes: [
|
160
|
+
{
|
161
|
+
id: 'no-lineage',
|
162
|
+
data: { label: 'No Lineage Generated' },
|
163
|
+
position: { x: 350, y: 300 },
|
164
|
+
type: 'default',
|
165
|
+
style: {
|
166
|
+
backgroundColor: '#f5f5f5',
|
167
|
+
border: '1px solid #ccc',
|
168
|
+
borderRadius: '5px',
|
169
|
+
padding: '10px',
|
170
|
+
width: 200,
|
171
|
+
},
|
172
|
+
},
|
173
|
+
],
|
174
|
+
edges: [],
|
175
|
+
bounds: { width: 800, height: 600 },
|
176
|
+
};
|
177
|
+
}
|
178
|
+
const nodeList = graph.nodes.map((node: LineageNode) => ({
|
179
|
+
id: node.data.id,
|
180
|
+
label: node.data.text || node.data.id,
|
181
|
+
}));
|
182
|
+
const edgeList = graph.edges.map((edge: LineageEdge) => ({
|
183
|
+
source: edge.data.source.data.id,
|
184
|
+
target: edge.data.target.data.id,
|
185
|
+
}));
|
186
|
+
const positions = autoLayoutNodesAndEdges(nodeList, edgeList);
|
187
|
+
const bounds = getLayoutBounds(positions);
|
188
|
+
|
189
|
+
// For class and database lineage, all nodes should have edges starting from right side
|
190
|
+
const nodes = nodeList.map((node) => ({
|
191
|
+
id: node.id,
|
192
|
+
data: { label: node.label },
|
193
|
+
position: positions[node.id] ?? { x: 0, y: 0 },
|
194
|
+
type: 'default' as const,
|
195
|
+
sourcePosition: Position.Right,
|
196
|
+
targetPosition: Position.Left,
|
197
|
+
}));
|
198
|
+
|
199
|
+
const edges = edgeList.map((edge, idx) => ({
|
200
|
+
id: `${edge.source}-${edge.target}-${idx}`,
|
201
|
+
source: edge.source,
|
202
|
+
target: edge.target,
|
203
|
+
type: 'smoothstep' as const,
|
204
|
+
// No explicit sourceHandle or targetHandle needed as we set sourcePosition and targetPosition on nodes
|
205
|
+
}));
|
206
|
+
return { nodes, edges, bounds };
|
207
|
+
};
|
208
|
+
|
209
|
+
const convertReportLineageToFlow = (reportLineage?: ReportLineage) => {
|
210
|
+
if (!reportLineage?.columns.length) {
|
211
|
+
// Handle missing or empty report lineage
|
212
|
+
return {
|
213
|
+
nodes: [
|
214
|
+
{
|
215
|
+
id: 'no-report-lineage',
|
216
|
+
data: { label: 'No Lineage Generated' },
|
217
|
+
position: { x: 350, y: 300 },
|
218
|
+
type: 'default',
|
219
|
+
style: {
|
220
|
+
backgroundColor: '#f5f5f5',
|
221
|
+
border: '1px solid #ccc',
|
222
|
+
borderRadius: '5px',
|
223
|
+
padding: '10px',
|
224
|
+
width: 200,
|
225
|
+
},
|
226
|
+
},
|
227
|
+
],
|
228
|
+
edges: [],
|
229
|
+
bounds: { width: 800, height: 600 },
|
230
|
+
};
|
231
|
+
}
|
232
|
+
|
233
|
+
const nodes: ReactFlowNode[] = [];
|
234
|
+
const edges: ReactFlowEdge[] = [];
|
235
|
+
|
236
|
+
// Layout constants
|
237
|
+
const ySpacing = 70;
|
238
|
+
const leftX = 100;
|
239
|
+
const rightX = 500;
|
240
|
+
const startY = 100;
|
241
|
+
const columnWidth = 180;
|
242
|
+
const tableWidth = 220;
|
243
|
+
const headerHeight = 40;
|
244
|
+
|
245
|
+
// Create report columns container
|
246
|
+
const reportContainerId = 'report_columns_container';
|
247
|
+
const reportContainerHeight =
|
248
|
+
reportLineage.columns.length * ySpacing + headerHeight;
|
249
|
+
nodes.push({
|
250
|
+
id: reportContainerId,
|
251
|
+
data: { label: 'Report Columns' },
|
252
|
+
position: { x: leftX, y: startY },
|
253
|
+
style: {
|
254
|
+
width: columnWidth,
|
255
|
+
height: reportContainerHeight,
|
256
|
+
backgroundColor: '#eaf6ff',
|
257
|
+
border: '2px solid #0073e6',
|
258
|
+
borderRadius: '8px',
|
259
|
+
padding: '10px',
|
260
|
+
textAlign: 'center',
|
261
|
+
fontWeight: 'bold',
|
262
|
+
},
|
263
|
+
type: 'default',
|
264
|
+
});
|
265
|
+
|
266
|
+
// Report columns inside the container
|
267
|
+
reportLineage.columns.forEach((col, idx) => {
|
268
|
+
const nodeId = `report_col_${col.name}`;
|
269
|
+
nodes.push({
|
270
|
+
id: nodeId,
|
271
|
+
data: { label: col.name },
|
272
|
+
position: { x: 10, y: headerHeight + idx * ySpacing },
|
273
|
+
parentId: reportContainerId,
|
274
|
+
extent: 'parent',
|
275
|
+
style: {
|
276
|
+
width: columnWidth - 20,
|
277
|
+
backgroundColor: '#f0f7ff',
|
278
|
+
border: '1px solid #0073e6',
|
279
|
+
borderRadius: '5px',
|
280
|
+
padding: '8px',
|
281
|
+
zIndex: 10,
|
282
|
+
},
|
283
|
+
type: 'default',
|
284
|
+
sourcePosition: Position.Right, // All report columns should have edges leaving from the right
|
285
|
+
targetPosition: Position.Left,
|
286
|
+
});
|
287
|
+
});
|
288
|
+
|
289
|
+
// Collect all unique owner/column pairs
|
290
|
+
const tables = new Map<
|
291
|
+
string,
|
292
|
+
{
|
293
|
+
owner: Owner;
|
294
|
+
columns: string[];
|
295
|
+
}
|
296
|
+
>();
|
297
|
+
|
298
|
+
reportLineage.columns.forEach((col) => {
|
299
|
+
col.columns.forEach((childCol) => {
|
300
|
+
const ownerObj = childCol.column.owner;
|
301
|
+
const ownerKey = `${ownerObj.schema.database.package}.${ownerObj.schema.database.name}.${ownerObj.schema.name}`;
|
302
|
+
const columnName = childCol.column.name;
|
303
|
+
|
304
|
+
if (!tables.has(ownerKey)) {
|
305
|
+
tables.set(ownerKey, {
|
306
|
+
owner: ownerObj,
|
307
|
+
columns: [],
|
308
|
+
});
|
309
|
+
}
|
310
|
+
|
311
|
+
const tableInfo = tables.get(ownerKey);
|
312
|
+
if (!tableInfo) {
|
313
|
+
return;
|
314
|
+
}
|
315
|
+
if (!tableInfo.columns.includes(columnName)) {
|
316
|
+
tableInfo.columns.push(columnName);
|
317
|
+
}
|
318
|
+
});
|
319
|
+
});
|
320
|
+
|
321
|
+
// Place tables and their columns
|
322
|
+
let currentY = startY;
|
323
|
+
const tableColumnPositions = new Map<string, { x: number; y: number }>();
|
324
|
+
tables.forEach(({ owner, columns }, ownerKey) => {
|
325
|
+
const tableId = `table_${ownerKey.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
326
|
+
const tableHeight = headerHeight + columns.length * ySpacing;
|
327
|
+
|
328
|
+
// Table container node
|
329
|
+
nodes.push({
|
330
|
+
id: tableId,
|
331
|
+
data: { label: owner.name },
|
332
|
+
position: { x: rightX, y: currentY },
|
333
|
+
style: {
|
334
|
+
width: tableWidth,
|
335
|
+
height: tableHeight,
|
336
|
+
backgroundColor: '#f5f5f5',
|
337
|
+
border: '2px solid #555',
|
338
|
+
borderRadius: '5px',
|
339
|
+
padding: '10px 0 0 0',
|
340
|
+
fontWeight: 'bold',
|
341
|
+
},
|
342
|
+
type: 'default',
|
343
|
+
});
|
344
|
+
|
345
|
+
// Column nodes within table
|
346
|
+
columns.forEach((column, colIdx) => {
|
347
|
+
const columnId = `${tableId}_column_${column}`;
|
348
|
+
const columnY = colIdx * ySpacing;
|
349
|
+
tableColumnPositions.set(columnId, { x: 10, y: headerHeight + columnY });
|
350
|
+
nodes.push({
|
351
|
+
id: columnId,
|
352
|
+
data: { label: column },
|
353
|
+
position: { x: 10, y: headerHeight + columnY },
|
354
|
+
parentId: tableId,
|
355
|
+
extent: 'parent',
|
356
|
+
style: {
|
357
|
+
width: tableWidth - 20,
|
358
|
+
backgroundColor: '#fff',
|
359
|
+
border: '1px solid #ccc',
|
360
|
+
borderRadius: '3px',
|
361
|
+
padding: '8px',
|
362
|
+
fontSize: '12px',
|
363
|
+
zIndex: 10,
|
364
|
+
},
|
365
|
+
type: 'default',
|
366
|
+
sourcePosition: Position.Right,
|
367
|
+
targetPosition: Position.Left, // All target columns should accept edges on the left
|
368
|
+
});
|
369
|
+
});
|
370
|
+
|
371
|
+
currentY += tableHeight + 50; // Add spacing between tables
|
372
|
+
});
|
373
|
+
|
374
|
+
// Create edges between report columns and table columns
|
375
|
+
reportLineage.columns.forEach((col) => {
|
376
|
+
const sourceId = `report_col_${col.name}`;
|
377
|
+
col.columns.forEach((childCol) => {
|
378
|
+
const ownerObj = childCol.column.owner;
|
379
|
+
const ownerKey = `${ownerObj.schema.database.package}.${ownerObj.schema.database.name}.${ownerObj.schema.name}`;
|
380
|
+
const columnName = childCol.column.name;
|
381
|
+
const tableId = `table_${ownerKey.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
382
|
+
const targetId = `${tableId}_column_${columnName}`;
|
383
|
+
|
384
|
+
edges.push({
|
385
|
+
id: `${sourceId}-${targetId}`,
|
386
|
+
source: sourceId,
|
387
|
+
target: targetId,
|
388
|
+
type: 'default',
|
389
|
+
style: { strokeWidth: 1.5 },
|
390
|
+
});
|
391
|
+
});
|
392
|
+
});
|
393
|
+
|
394
|
+
// Determine total height needed
|
395
|
+
const totalHeight = Math.max(startY + reportContainerHeight + 100, currentY);
|
396
|
+
|
397
|
+
return {
|
398
|
+
nodes,
|
399
|
+
edges,
|
400
|
+
bounds: {
|
401
|
+
width: rightX + tableWidth + 100,
|
402
|
+
height: totalHeight,
|
403
|
+
},
|
404
|
+
};
|
405
|
+
};
|
406
|
+
|
407
|
+
// Graph Viewer Component
|
408
|
+
const LineageGraphViewer = observer(
|
409
|
+
(props: { nodes: ReactFlowNode[]; edges: ReactFlowEdge[] }) => {
|
410
|
+
const { nodes, edges } = props;
|
411
|
+
return (
|
412
|
+
<div style={{ height: '100%', width: '100%' }}>
|
413
|
+
<ReactFlowProvider>
|
414
|
+
<div style={{ width: '100%', height: '100%' }}>
|
415
|
+
<ReactFlow
|
416
|
+
nodes={nodes}
|
417
|
+
edges={edges}
|
418
|
+
defaultEdgeOptions={{ type: 'default' }}
|
419
|
+
defaultViewport={{ x: 0, y: 0, zoom: 1.5 }}
|
420
|
+
fitView={true}
|
421
|
+
nodesDraggable={true}
|
422
|
+
>
|
423
|
+
<Background />
|
424
|
+
<MiniMap />
|
425
|
+
<Controls />
|
426
|
+
</ReactFlow>
|
427
|
+
</div>
|
428
|
+
</ReactFlowProvider>
|
429
|
+
</div>
|
430
|
+
);
|
431
|
+
},
|
432
|
+
);
|
433
|
+
|
434
|
+
const TAB_ORDER = [
|
435
|
+
LINEAGE_VIEW_MODE.DATABASE_LINEAGE,
|
436
|
+
LINEAGE_VIEW_MODE.CLASS_LINEAGE,
|
437
|
+
LINEAGE_VIEW_MODE.REPORT_LINEAGE,
|
438
|
+
];
|
439
|
+
|
440
|
+
const TAB_LABELS: Record<LINEAGE_VIEW_MODE, string> = {
|
441
|
+
[LINEAGE_VIEW_MODE.CLASS_LINEAGE]: 'Class Lineage',
|
442
|
+
[LINEAGE_VIEW_MODE.DATABASE_LINEAGE]: 'Database Lineage',
|
443
|
+
[LINEAGE_VIEW_MODE.REPORT_LINEAGE]: 'Report Lineage',
|
444
|
+
};
|
445
|
+
|
446
|
+
const LineageTabSelector = observer((props: { lineageState: LineageState }) => {
|
447
|
+
const { lineageState } = props;
|
448
|
+
return (
|
449
|
+
<div className="panel__header query-builder__execution-plan-form--editor__header--with-tabs">
|
450
|
+
<div className="uml-element-editor__tabs">
|
451
|
+
{TAB_ORDER.map((tab) => (
|
452
|
+
<div
|
453
|
+
key={tab}
|
454
|
+
onClick={() => lineageState.setSelectedTab(tab)}
|
455
|
+
className={clsx('query-builder__execution-plan-form--editor__tab', {
|
456
|
+
'query-builder__execution-plan-form--editor__tab--active':
|
457
|
+
tab === lineageState.selectedTab,
|
458
|
+
})}
|
459
|
+
>
|
460
|
+
{TAB_LABELS[tab]}
|
461
|
+
</div>
|
462
|
+
))}
|
463
|
+
</div>
|
464
|
+
</div>
|
465
|
+
);
|
466
|
+
});
|
467
|
+
|
468
|
+
const LineageViewerContent = observer(
|
469
|
+
(props: { lineageState: LineageState }) => {
|
470
|
+
const { lineageState } = props;
|
471
|
+
const selectedTab = lineageState.selectedTab;
|
472
|
+
const lineageData = lineageState.lineageData;
|
473
|
+
|
474
|
+
// Prepare all three graphs
|
475
|
+
const classLineageFlow = convertGraphToFlow(lineageData?.classLineage);
|
476
|
+
const databaseLineageFlow = convertGraphToFlow(
|
477
|
+
lineageData?.databaseLineage,
|
478
|
+
);
|
479
|
+
const reportLineageFlow = convertReportLineageToFlow(
|
480
|
+
lineageData?.reportLineage,
|
481
|
+
);
|
482
|
+
|
483
|
+
return (
|
484
|
+
<div
|
485
|
+
className="query-builder__execution-plan-form--editor"
|
486
|
+
style={{ height: '100%' }}
|
487
|
+
>
|
488
|
+
<div className="panel" style={{ height: '100%' }}>
|
489
|
+
<LineageTabSelector lineageState={lineageState} />
|
490
|
+
<PanelContent>
|
491
|
+
{selectedTab === LINEAGE_VIEW_MODE.CLASS_LINEAGE && (
|
492
|
+
<LineageGraphViewer
|
493
|
+
nodes={classLineageFlow.nodes}
|
494
|
+
edges={classLineageFlow.edges}
|
495
|
+
/>
|
496
|
+
)}
|
497
|
+
{selectedTab === LINEAGE_VIEW_MODE.DATABASE_LINEAGE && (
|
498
|
+
<LineageGraphViewer
|
499
|
+
nodes={databaseLineageFlow.nodes}
|
500
|
+
edges={databaseLineageFlow.edges}
|
501
|
+
/>
|
502
|
+
)}
|
503
|
+
{selectedTab === LINEAGE_VIEW_MODE.REPORT_LINEAGE && (
|
504
|
+
<LineageGraphViewer
|
505
|
+
nodes={reportLineageFlow.nodes}
|
506
|
+
edges={reportLineageFlow.edges}
|
507
|
+
/>
|
508
|
+
)}
|
509
|
+
</PanelContent>
|
510
|
+
</div>
|
511
|
+
</div>
|
512
|
+
);
|
513
|
+
},
|
514
|
+
);
|
515
|
+
|
516
|
+
export const LineageViewer = observer(
|
517
|
+
(props: { lineageState: LineageState }) => {
|
518
|
+
const { lineageState } = props;
|
519
|
+
|
520
|
+
const closePlanViewer = (): void => {
|
521
|
+
lineageState.setLineageData(undefined);
|
522
|
+
lineageState.setSelectedTab(LINEAGE_VIEW_MODE.DATABASE_LINEAGE);
|
523
|
+
};
|
524
|
+
|
525
|
+
useEffect(() => {
|
526
|
+
lineageState.setSelectedTab(LINEAGE_VIEW_MODE.DATABASE_LINEAGE);
|
527
|
+
}, [lineageState]);
|
528
|
+
|
529
|
+
if (!lineageState.lineageData) {
|
530
|
+
return null;
|
531
|
+
}
|
532
|
+
const isDarkMode =
|
533
|
+
!lineageState.applicationStore.layoutService
|
534
|
+
.TEMPORARY__isLightColorThemeEnabled;
|
535
|
+
return (
|
536
|
+
<Dialog
|
537
|
+
open={Boolean(lineageState.lineageData)}
|
538
|
+
onClose={closePlanViewer}
|
539
|
+
>
|
540
|
+
<Modal className="editor-modal" darkMode={isDarkMode}>
|
541
|
+
<ModalHeader title="Lineage Viewer" />
|
542
|
+
<ModalBody>
|
543
|
+
<div
|
544
|
+
className="query-builder__execution-plan"
|
545
|
+
style={{ height: '100%' }}
|
546
|
+
>
|
547
|
+
<LineageViewerContent lineageState={lineageState} />
|
548
|
+
</div>
|
549
|
+
</ModalBody>
|
550
|
+
<ModalFooter className="editor-modal__footer">
|
551
|
+
<ModalFooterButton
|
552
|
+
onClick={closePlanViewer}
|
553
|
+
text="Close"
|
554
|
+
type="secondary"
|
555
|
+
/>
|
556
|
+
</ModalFooter>
|
557
|
+
</Modal>
|
558
|
+
</Dialog>
|
559
|
+
);
|
560
|
+
},
|
561
|
+
);
|
package/src/index.ts
CHANGED
@@ -127,7 +127,10 @@ export * from './stores/shared/V1_ValueSpecificationModifierHelper.js';
|
|
127
127
|
export * from './stores/shared/V1_ValueSpecificationEditorHelper.js';
|
128
128
|
|
129
129
|
export * from './components/execution-plan/ExecutionPlanViewer.js';
|
130
|
+
export * from './components/lineage/LineageViewer.js';
|
131
|
+
|
130
132
|
export * from './stores/execution-plan/ExecutionPlanState.js';
|
133
|
+
export * from './stores/lineage/LineageState.js';
|
131
134
|
|
132
135
|
export * from './components/QueryLoader.js';
|
133
136
|
export * from './components/QueryBuilderDiffPanel.js';
|
@@ -0,0 +1,56 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright (c) 2020-present, Goldman Sachs
|
3
|
+
*
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
* you may not use this file except in compliance with the License.
|
6
|
+
* You may obtain a copy of the License at
|
7
|
+
*
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
*
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
* See the License for the specific language governing permissions and
|
14
|
+
* limitations under the License.
|
15
|
+
*/
|
16
|
+
|
17
|
+
import { observable, action, makeObservable } from 'mobx';
|
18
|
+
import type { GenericLegendApplicationStore } from '@finos/legend-application';
|
19
|
+
import type { LineageModel } from '@finos/legend-graph';
|
20
|
+
|
21
|
+
export enum LINEAGE_VIEW_MODE {
|
22
|
+
CLASS_LINEAGE = 'CLASS_LINEAGE',
|
23
|
+
DATABASE_LINEAGE = 'DATABASE_LINEAGE',
|
24
|
+
REPORT_LINEAGE = 'REPORT_LINEAGE',
|
25
|
+
}
|
26
|
+
|
27
|
+
export class LineageState {
|
28
|
+
applicationStore: GenericLegendApplicationStore;
|
29
|
+
selectedTab: LINEAGE_VIEW_MODE = LINEAGE_VIEW_MODE.DATABASE_LINEAGE;
|
30
|
+
lineageData: LineageModel | undefined = undefined;
|
31
|
+
isLineageViewerOpen = false;
|
32
|
+
|
33
|
+
constructor(applicationStore: GenericLegendApplicationStore) {
|
34
|
+
makeObservable(this, {
|
35
|
+
selectedTab: observable,
|
36
|
+
lineageData: observable,
|
37
|
+
isLineageViewerOpen: observable,
|
38
|
+
setSelectedTab: action,
|
39
|
+
setLineageData: action,
|
40
|
+
setIsLineageViewerOpen: action,
|
41
|
+
});
|
42
|
+
this.applicationStore = applicationStore;
|
43
|
+
}
|
44
|
+
|
45
|
+
setSelectedTab(tab: LINEAGE_VIEW_MODE): void {
|
46
|
+
this.selectedTab = tab;
|
47
|
+
}
|
48
|
+
|
49
|
+
setLineageData(data: LineageModel | undefined): void {
|
50
|
+
this.lineageData = data;
|
51
|
+
}
|
52
|
+
|
53
|
+
setIsLineageViewerOpen(isOpen: boolean): void {
|
54
|
+
this.isLineageViewerOpen = isOpen;
|
55
|
+
}
|
56
|
+
}
|
package/tsconfig.json
CHANGED
@@ -198,6 +198,7 @@
|
|
198
198
|
"./src/stores/filter/operators/QueryBuilderFilterOperator_LessThan.ts",
|
199
199
|
"./src/stores/filter/operators/QueryBuilderFilterOperator_LessThanEqual.ts",
|
200
200
|
"./src/stores/filter/operators/QueryBuilderFilterOperator_StartWith.ts",
|
201
|
+
"./src/stores/lineage/LineageState.ts",
|
201
202
|
"./src/stores/milestoning/QueryBuilderBitemporalMilestoningImplementation.ts",
|
202
203
|
"./src/stores/milestoning/QueryBuilderBusinessTemporalMilestoningImplementation.ts",
|
203
204
|
"./src/stores/milestoning/QueryBuilderMilestoningHelper.ts",
|
@@ -265,6 +266,7 @@
|
|
265
266
|
"./src/components/fetch-structure/QueryBuilderTDSPanel.tsx",
|
266
267
|
"./src/components/fetch-structure/QueryBuilderTDSWindowPanel.tsx",
|
267
268
|
"./src/components/filter/QueryBuilderFilterPanel.tsx",
|
269
|
+
"./src/components/lineage/LineageViewer.tsx",
|
268
270
|
"./src/components/result/QueryBuilderResultPanel.tsx",
|
269
271
|
"./src/components/result/tds/QueryBuilderTDSGridResult.tsx",
|
270
272
|
"./src/components/result/tds/QueryBuilderTDSResultShared.tsx",
|