@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.
@@ -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",