@finos/legend-query-builder 4.17.25 → 4.17.27
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.map +1 -1
- package/lib/components/lineage/LineageViewer.js +302 -7
- package/lib/components/lineage/LineageViewer.js.map +1 -1
- package/lib/components/lineage/PropertyOwnerNode.d.ts +31 -0
- package/lib/components/lineage/PropertyOwnerNode.d.ts.map +1 -0
- package/lib/components/lineage/PropertyOwnerNode.js +58 -0
- package/lib/components/lineage/PropertyOwnerNode.js.map +1 -0
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/package.json +1 -1
- package/lib/stores/lineage/LineageState.d.ts +9 -1
- package/lib/stores/lineage/LineageState.d.ts.map +1 -1
- package/lib/stores/lineage/LineageState.js +25 -0
- package/lib/stores/lineage/LineageState.js.map +1 -1
- package/package.json +7 -7
- package/src/components/lineage/LineageViewer.tsx +542 -12
- package/src/components/lineage/PropertyOwnerNode.tsx +145 -0
- package/src/stores/lineage/LineageState.ts +31 -0
- package/tsconfig.json +1 -0
@@ -14,40 +14,49 @@
|
|
14
14
|
* limitations under the License.
|
15
15
|
*/
|
16
16
|
|
17
|
-
import { useEffect } from 'react';
|
17
|
+
import React, { useEffect } from 'react';
|
18
18
|
import {
|
19
|
-
PanelContent,
|
20
19
|
clsx,
|
21
20
|
Dialog,
|
22
21
|
Modal,
|
23
|
-
ModalHeader,
|
24
22
|
ModalBody,
|
25
23
|
ModalFooter,
|
26
24
|
ModalFooterButton,
|
25
|
+
ModalHeader,
|
26
|
+
PanelContent,
|
27
27
|
} from '@finos/legend-art';
|
28
28
|
import { observer } from 'mobx-react-lite';
|
29
29
|
import {
|
30
|
-
ReactFlow,
|
31
30
|
Background,
|
32
31
|
Controls,
|
32
|
+
type Edge as ReactFlowEdge,
|
33
33
|
MiniMap,
|
34
|
-
ReactFlowProvider,
|
35
|
-
Position,
|
36
34
|
type Node as ReactFlowNode,
|
37
|
-
|
35
|
+
Position,
|
36
|
+
ReactFlow,
|
37
|
+
ReactFlowProvider,
|
38
38
|
} from 'reactflow';
|
39
39
|
import {
|
40
|
-
type LineageState,
|
41
40
|
LINEAGE_VIEW_MODE,
|
41
|
+
type LineageState,
|
42
42
|
} from '../../stores/lineage/LineageState.js';
|
43
43
|
|
44
44
|
import {
|
45
45
|
type Graph,
|
46
|
+
type LineageEdge,
|
47
|
+
type LineageNode,
|
48
|
+
type LineageProperty,
|
46
49
|
type Owner,
|
50
|
+
type OwnerLink,
|
51
|
+
type PropertyLineageNode,
|
52
|
+
type PropertyLineageReport,
|
53
|
+
PropertyOwnerNode,
|
47
54
|
type ReportLineage,
|
48
|
-
type LineageNode,
|
49
|
-
type LineageEdge,
|
50
55
|
} from '@finos/legend-graph';
|
56
|
+
import {
|
57
|
+
PropertyOwnerNode as PropertyOwnerNodeComponent,
|
58
|
+
type PropertyOwnerNodeData,
|
59
|
+
} from './PropertyOwnerNode.js';
|
51
60
|
|
52
61
|
function autoLayoutNodesAndEdges<T extends { id: string }>(
|
53
62
|
nodes: T[],
|
@@ -177,7 +186,7 @@ const convertGraphToFlow = (graph?: Graph) => {
|
|
177
186
|
}
|
178
187
|
const nodeList = graph.nodes.map((node: LineageNode) => ({
|
179
188
|
id: node.data.id,
|
180
|
-
label: node.data.text
|
189
|
+
label: node.data.text,
|
181
190
|
}));
|
182
191
|
const edgeList = graph.edges.map((edge: LineageEdge) => ({
|
183
192
|
source: edge.data.source.data.id,
|
@@ -404,6 +413,514 @@ const convertReportLineageToFlow = (reportLineage?: ReportLineage) => {
|
|
404
413
|
};
|
405
414
|
};
|
406
415
|
|
416
|
+
const convertPropertyLineageToFlow = (
|
417
|
+
propertyLineage?: PropertyLineageReport,
|
418
|
+
selectedSourcePropertiesMap?: Map<string, Set<string>>,
|
419
|
+
) => {
|
420
|
+
if (!propertyLineage?.propertyOwner.length) {
|
421
|
+
return {
|
422
|
+
nodes: [
|
423
|
+
{
|
424
|
+
id: 'no-property-lineage',
|
425
|
+
data: { label: 'No Property Lineage Generated' },
|
426
|
+
position: { x: 350, y: 300 },
|
427
|
+
type: 'default',
|
428
|
+
style: {
|
429
|
+
backgroundColor: '#f5f5f5',
|
430
|
+
border: '1px solid #ccc',
|
431
|
+
borderRadius: '5px',
|
432
|
+
padding: '10px',
|
433
|
+
width: 200,
|
434
|
+
},
|
435
|
+
},
|
436
|
+
],
|
437
|
+
edges: [],
|
438
|
+
bounds: { width: 800, height: 600 },
|
439
|
+
};
|
440
|
+
}
|
441
|
+
|
442
|
+
const nodeList = propertyLineage.propertyOwner.map(
|
443
|
+
(node: PropertyLineageNode) => ({
|
444
|
+
id: node.id,
|
445
|
+
label: node.name,
|
446
|
+
isPropertyOwner: node instanceof PropertyOwnerNode,
|
447
|
+
node: node,
|
448
|
+
}),
|
449
|
+
);
|
450
|
+
|
451
|
+
const edgeList = propertyLineage.ownerLink.map((link: OwnerLink) => ({
|
452
|
+
source: link.source,
|
453
|
+
target: link.target,
|
454
|
+
}));
|
455
|
+
|
456
|
+
const nodeIds = new Set(nodeList.map((n) => n.id));
|
457
|
+
const validEdgeList = edgeList.filter((edge) => {
|
458
|
+
return !(!nodeIds.has(edge.source) || !nodeIds.has(edge.target));
|
459
|
+
});
|
460
|
+
|
461
|
+
const nodeDimensions = new Map<string, { width: number; height: number }>();
|
462
|
+
|
463
|
+
nodeList.forEach((nodeItem) => {
|
464
|
+
const isPropertyOwner = nodeItem.isPropertyOwner;
|
465
|
+
const highlightedProperties = selectedSourcePropertiesMap?.get(nodeItem.id);
|
466
|
+
const hasHighlightedProperties = (highlightedProperties?.size ?? 0) > 0;
|
467
|
+
let nodeWidth = 220;
|
468
|
+
let nodeHeight = isPropertyOwner ? 80 : 60;
|
469
|
+
|
470
|
+
if (hasHighlightedProperties && isPropertyOwner) {
|
471
|
+
const propertyCount = Math.min(highlightedProperties?.size ?? 0, 20);
|
472
|
+
|
473
|
+
// Header (50px) + properties (32px each including margins) + container padding (20px)
|
474
|
+
const propertiesHeight = propertyCount * 32; // 28px min-height + 4px margin
|
475
|
+
|
476
|
+
nodeHeight = 50 + propertiesHeight + 20;
|
477
|
+
nodeHeight = Math.max(nodeHeight, 160);
|
478
|
+
nodeHeight = Math.min(nodeHeight, 800);
|
479
|
+
|
480
|
+
nodeWidth = 340;
|
481
|
+
}
|
482
|
+
|
483
|
+
nodeDimensions.set(nodeItem.id, { width: nodeWidth, height: nodeHeight });
|
484
|
+
});
|
485
|
+
|
486
|
+
const maxHeight = Math.max(
|
487
|
+
...Array.from(nodeDimensions.values()).map((d) => d.height),
|
488
|
+
);
|
489
|
+
const dynamicYSpacing = Math.max(220, maxHeight + 100);
|
490
|
+
const dynamicXSpacing = 380;
|
491
|
+
|
492
|
+
const positions = autoLayoutNodesAndEdges(
|
493
|
+
nodeList,
|
494
|
+
validEdgeList,
|
495
|
+
dynamicXSpacing,
|
496
|
+
dynamicYSpacing,
|
497
|
+
);
|
498
|
+
const bounds = getLayoutBounds(positions);
|
499
|
+
|
500
|
+
const nodes = nodeList.map((nodeItem) => {
|
501
|
+
const isPropertyOwner = nodeItem.isPropertyOwner;
|
502
|
+
const highlightedProperties = selectedSourcePropertiesMap?.get(nodeItem.id);
|
503
|
+
|
504
|
+
let allProperties: Array<{
|
505
|
+
name: string;
|
506
|
+
dataType?: string | undefined;
|
507
|
+
propertyType: string | undefined;
|
508
|
+
}> = [];
|
509
|
+
if (isPropertyOwner && nodeItem.node instanceof PropertyOwnerNode) {
|
510
|
+
const properties = nodeItem.node.properties;
|
511
|
+
allProperties = properties.map((prop) => ({
|
512
|
+
name: prop.name,
|
513
|
+
dataType: prop.dataType,
|
514
|
+
propertyType: prop.propertyType,
|
515
|
+
}));
|
516
|
+
}
|
517
|
+
|
518
|
+
const dimensions = nodeDimensions.get(nodeItem.id) ?? {
|
519
|
+
width: 220,
|
520
|
+
height: 80,
|
521
|
+
};
|
522
|
+
|
523
|
+
const nodeData: PropertyOwnerNodeData = {
|
524
|
+
label: nodeItem.label,
|
525
|
+
isPropertyOwner: isPropertyOwner,
|
526
|
+
highlightedProperties: highlightedProperties,
|
527
|
+
allProperties: allProperties,
|
528
|
+
};
|
529
|
+
|
530
|
+
return {
|
531
|
+
id: nodeItem.id,
|
532
|
+
data: nodeData,
|
533
|
+
position: positions[nodeItem.id] ?? { x: 0, y: 0 },
|
534
|
+
type: isPropertyOwner ? 'propertyOwner' : 'default',
|
535
|
+
sourcePosition: Position.Right,
|
536
|
+
targetPosition: Position.Left,
|
537
|
+
style: {
|
538
|
+
width: dimensions.width,
|
539
|
+
height: dimensions.height,
|
540
|
+
background: 'transparent',
|
541
|
+
border: 'none',
|
542
|
+
padding: 0,
|
543
|
+
margin: 0,
|
544
|
+
},
|
545
|
+
width: dimensions.width,
|
546
|
+
height: dimensions.height,
|
547
|
+
};
|
548
|
+
});
|
549
|
+
|
550
|
+
const edges = validEdgeList.map((edge, idx) => ({
|
551
|
+
id: `${edge.source}-${edge.target}-${idx}`,
|
552
|
+
source: edge.source,
|
553
|
+
target: edge.target,
|
554
|
+
type: 'smoothstep' as const,
|
555
|
+
style: { strokeWidth: 2, stroke: '#1976d2' },
|
556
|
+
}));
|
557
|
+
|
558
|
+
const expandedBounds = {
|
559
|
+
width: Math.max(bounds.width, 1200),
|
560
|
+
height: Math.max(bounds.height, 800),
|
561
|
+
};
|
562
|
+
|
563
|
+
return { nodes, edges, bounds: expandedBounds };
|
564
|
+
};
|
565
|
+
|
566
|
+
const collectSourceOwnerProperties = (
|
567
|
+
property: LineageProperty,
|
568
|
+
map: Map<string, Set<string>>,
|
569
|
+
visited: Set<string> = new Set(),
|
570
|
+
depth: number = 0,
|
571
|
+
): void => {
|
572
|
+
if (depth > 50) {
|
573
|
+
return;
|
574
|
+
}
|
575
|
+
|
576
|
+
if (
|
577
|
+
!Array.isArray(property.sourceProperties) ||
|
578
|
+
property.sourceProperties.length === 0
|
579
|
+
) {
|
580
|
+
return;
|
581
|
+
}
|
582
|
+
|
583
|
+
const propertyKey = `${property.ownerID}-${property.name}`;
|
584
|
+
|
585
|
+
if (visited.has(propertyKey)) {
|
586
|
+
return;
|
587
|
+
}
|
588
|
+
|
589
|
+
visited.add(propertyKey);
|
590
|
+
|
591
|
+
try {
|
592
|
+
property.sourceProperties.forEach((sourceProp) => {
|
593
|
+
if (!map.has(sourceProp.ownerID)) {
|
594
|
+
map.set(sourceProp.ownerID, new Set());
|
595
|
+
}
|
596
|
+
const sourceSet = map.get(sourceProp.ownerID);
|
597
|
+
if (sourceSet) {
|
598
|
+
sourceSet.add(sourceProp.name);
|
599
|
+
}
|
600
|
+
collectSourceOwnerProperties(sourceProp, map, visited, depth + 1);
|
601
|
+
});
|
602
|
+
} catch {
|
603
|
+
return;
|
604
|
+
} finally {
|
605
|
+
visited.delete(propertyKey);
|
606
|
+
}
|
607
|
+
};
|
608
|
+
|
609
|
+
const findRelevantEdges = (
|
610
|
+
highlightedNodeIds: Set<string>,
|
611
|
+
allEdges: ReactFlowEdge[],
|
612
|
+
): Set<string> => {
|
613
|
+
const relevantEdgeIds = new Set<string>();
|
614
|
+
|
615
|
+
allEdges.forEach((edge) => {
|
616
|
+
if (
|
617
|
+
highlightedNodeIds.has(edge.source) &&
|
618
|
+
highlightedNodeIds.has(edge.target)
|
619
|
+
) {
|
620
|
+
relevantEdgeIds.add(edge.id);
|
621
|
+
}
|
622
|
+
});
|
623
|
+
|
624
|
+
return relevantEdgeIds;
|
625
|
+
};
|
626
|
+
|
627
|
+
const PROPERTY_LINEAGE_NODE_TYPES = {
|
628
|
+
propertyOwner: PropertyOwnerNodeComponent,
|
629
|
+
};
|
630
|
+
|
631
|
+
const PropertyOwnerPanel = observer(
|
632
|
+
(props: { lineageState: LineageState; selectedNodeId?: string }) => {
|
633
|
+
const { lineageState, selectedNodeId } = props;
|
634
|
+
const propertyLineage = lineageState.lineageData?.propertyLineage;
|
635
|
+
|
636
|
+
if (!selectedNodeId || !propertyLineage) {
|
637
|
+
return (
|
638
|
+
<div className="property-lineage__panel">
|
639
|
+
<div className="property-lineage__panel-header">
|
640
|
+
<h3>Properties</h3>
|
641
|
+
</div>
|
642
|
+
<div className="property-lineage__panel-content">
|
643
|
+
<p>Select a node with properties to view details</p>
|
644
|
+
</div>
|
645
|
+
</div>
|
646
|
+
);
|
647
|
+
}
|
648
|
+
|
649
|
+
const selectedNode = propertyLineage.propertyOwner.find(
|
650
|
+
(node) => node.id === selectedNodeId,
|
651
|
+
);
|
652
|
+
const isPropertyOwner = selectedNode instanceof PropertyOwnerNode;
|
653
|
+
|
654
|
+
if (!isPropertyOwner) {
|
655
|
+
return (
|
656
|
+
<div className="property-lineage__panel">
|
657
|
+
<div className="property-lineage__panel-header">
|
658
|
+
<h3>Properties</h3>
|
659
|
+
</div>
|
660
|
+
<div className="property-lineage__panel-content">
|
661
|
+
<p>Selected node has no properties</p>
|
662
|
+
</div>
|
663
|
+
</div>
|
664
|
+
);
|
665
|
+
}
|
666
|
+
|
667
|
+
const propertyOwnerNode = selectedNode;
|
668
|
+
|
669
|
+
const handlePropertyClick = (property: LineageProperty) => {
|
670
|
+
const propertyKey = `${property.ownerID}-${property.name}`;
|
671
|
+
const currentSelection = lineageState.selectedProperty;
|
672
|
+
|
673
|
+
lineageState.setSelectedProperty(
|
674
|
+
currentSelection === propertyKey ? undefined : propertyKey,
|
675
|
+
);
|
676
|
+
if (currentSelection === propertyKey) {
|
677
|
+
lineageState.setSelectedSourcePropertiesMap(undefined);
|
678
|
+
} else {
|
679
|
+
const map = new Map<string, Set<string>>();
|
680
|
+
collectSourceOwnerProperties(property, map, new Set(), 0);
|
681
|
+
lineageState.setSelectedSourcePropertiesMap(map);
|
682
|
+
}
|
683
|
+
};
|
684
|
+
|
685
|
+
const properties = propertyOwnerNode.properties;
|
686
|
+
|
687
|
+
let highlightedSourceProps: Set<string> | undefined = undefined;
|
688
|
+
if (lineageState.selectedSourcePropertiesMap?.has(propertyOwnerNode.id)) {
|
689
|
+
highlightedSourceProps = lineageState.selectedSourcePropertiesMap.get(
|
690
|
+
propertyOwnerNode.id,
|
691
|
+
);
|
692
|
+
}
|
693
|
+
|
694
|
+
return (
|
695
|
+
<div className="property-lineage__panel">
|
696
|
+
<div className="property-lineage__panel-header">
|
697
|
+
<h3>{propertyOwnerNode.name}</h3>
|
698
|
+
<span className="property-lineage__panel-subtitle">
|
699
|
+
{properties.length} properties
|
700
|
+
</span>
|
701
|
+
</div>
|
702
|
+
<div className="property-lineage__panel-content">
|
703
|
+
{properties.length === 0 ? (
|
704
|
+
<p>No properties found</p>
|
705
|
+
) : (
|
706
|
+
<div className="property-lineage__properties-list">
|
707
|
+
{properties.map((property) => {
|
708
|
+
const propertyKey = `${property.ownerID}-${property.name}`;
|
709
|
+
const isSelected =
|
710
|
+
lineageState.selectedProperty === propertyKey;
|
711
|
+
|
712
|
+
const isSourceHighlighted = highlightedSourceProps?.has(
|
713
|
+
property.name,
|
714
|
+
);
|
715
|
+
|
716
|
+
return (
|
717
|
+
<div
|
718
|
+
key={propertyKey}
|
719
|
+
className={clsx('property-lineage__property-item', {
|
720
|
+
'property-lineage__property-item--selected': isSelected,
|
721
|
+
'property-lineage__property-item--source-highlighted':
|
722
|
+
isSourceHighlighted,
|
723
|
+
})}
|
724
|
+
onClick={() => handlePropertyClick(property)}
|
725
|
+
style={{ cursor: 'pointer' }}
|
726
|
+
>
|
727
|
+
<div className="property-lineage__property-name">
|
728
|
+
{property.name}
|
729
|
+
</div>
|
730
|
+
<div className="property-lineage__property-details">
|
731
|
+
<span className="property-lineage__property-type">
|
732
|
+
{property.dataType ?? 'Unknown'}
|
733
|
+
</span>
|
734
|
+
<span className="property-lineage__property-scope">
|
735
|
+
{property.propertyType}
|
736
|
+
</span>
|
737
|
+
</div>
|
738
|
+
{property.scope && (
|
739
|
+
<div className="property-lineage__property-scope-detail">
|
740
|
+
{property.scope}
|
741
|
+
</div>
|
742
|
+
)}
|
743
|
+
{Array.isArray(property.sourceProperties) &&
|
744
|
+
property.sourceProperties.length > 0 && (
|
745
|
+
<div className="property-lineage__property-sources">
|
746
|
+
{property.sourceProperties.length} source properties
|
747
|
+
</div>
|
748
|
+
)}
|
749
|
+
</div>
|
750
|
+
);
|
751
|
+
})}
|
752
|
+
</div>
|
753
|
+
)}
|
754
|
+
</div>
|
755
|
+
</div>
|
756
|
+
);
|
757
|
+
},
|
758
|
+
);
|
759
|
+
|
760
|
+
const PropertyLineageGraphViewer = observer(
|
761
|
+
(props: {
|
762
|
+
lineageState: LineageState;
|
763
|
+
nodes: ReactFlowNode[];
|
764
|
+
edges: ReactFlowEdge[];
|
765
|
+
}) => {
|
766
|
+
const { lineageState, nodes, edges } = props;
|
767
|
+
|
768
|
+
const getHighlightMaps = () => {
|
769
|
+
const highlightedNodeIds = new Set<string>();
|
770
|
+
|
771
|
+
const sourcePropertiesMap =
|
772
|
+
lineageState.selectedSourcePropertiesMap ??
|
773
|
+
new Map<string, Set<string>>();
|
774
|
+
|
775
|
+
if (
|
776
|
+
!lineageState.selectedProperty ||
|
777
|
+
!lineageState.selectedPropertyOwnerNode
|
778
|
+
) {
|
779
|
+
return { highlightedNodeIds, sourcePropertiesMap };
|
780
|
+
}
|
781
|
+
|
782
|
+
const propertyLineage = lineageState.lineageData?.propertyLineage;
|
783
|
+
if (!propertyLineage) {
|
784
|
+
return { highlightedNodeIds, sourcePropertiesMap };
|
785
|
+
}
|
786
|
+
|
787
|
+
const selectedNode = propertyLineage.propertyOwner.find(
|
788
|
+
(node) => node.id === lineageState.selectedPropertyOwnerNode,
|
789
|
+
);
|
790
|
+
|
791
|
+
if (!(selectedNode instanceof PropertyOwnerNode)) {
|
792
|
+
return { highlightedNodeIds, sourcePropertiesMap };
|
793
|
+
}
|
794
|
+
|
795
|
+
const [ownerID, propertyName] = lineageState.selectedProperty.split('-');
|
796
|
+
const selectedProperty = selectedNode.properties.find(
|
797
|
+
(prop) => prop.ownerID === ownerID && prop.name === propertyName,
|
798
|
+
);
|
799
|
+
|
800
|
+
if (selectedProperty) {
|
801
|
+
highlightedNodeIds.add(selectedProperty.ownerID);
|
802
|
+
|
803
|
+
for (const ownerId of sourcePropertiesMap.keys()) {
|
804
|
+
highlightedNodeIds.add(ownerId);
|
805
|
+
}
|
806
|
+
}
|
807
|
+
|
808
|
+
return { highlightedNodeIds, sourcePropertiesMap };
|
809
|
+
};
|
810
|
+
|
811
|
+
const { highlightedNodeIds, sourcePropertiesMap } = getHighlightMaps();
|
812
|
+
|
813
|
+
const onNodeClick = (
|
814
|
+
_event: React.MouseEvent<Element, MouseEvent>,
|
815
|
+
node: ReactFlowNode,
|
816
|
+
) => {
|
817
|
+
if (node.data.isPropertyOwner) {
|
818
|
+
lineageState.setSelectedPropertyOwnerNode(
|
819
|
+
lineageState.selectedPropertyOwnerNode === node.id
|
820
|
+
? undefined
|
821
|
+
: node.id,
|
822
|
+
);
|
823
|
+
lineageState.setSelectedProperty(undefined);
|
824
|
+
lineageState.setSelectedSourcePropertiesMap(undefined);
|
825
|
+
}
|
826
|
+
};
|
827
|
+
|
828
|
+
const enhancedNodes = nodes.map((node) => {
|
829
|
+
const isSelected = lineageState.selectedPropertyOwnerNode === node.id;
|
830
|
+
// Remove unnecessary type annotation
|
831
|
+
const isHighlighted = highlightedNodeIds.has(node.id);
|
832
|
+
|
833
|
+
// Add type for updatedData to avoid unsafe assignment
|
834
|
+
const updatedData: PropertyOwnerNodeData = {
|
835
|
+
...(node.data as PropertyOwnerNodeData),
|
836
|
+
isSelected,
|
837
|
+
isHighlighted,
|
838
|
+
};
|
839
|
+
return {
|
840
|
+
...node,
|
841
|
+
data: updatedData,
|
842
|
+
style: {
|
843
|
+
...node.style,
|
844
|
+
cursor: node.data.isPropertyOwner ? 'pointer' : 'default',
|
845
|
+
background:
|
846
|
+
node.type === 'propertyOwner'
|
847
|
+
? 'transparent'
|
848
|
+
: node.style?.backgroundColor,
|
849
|
+
border:
|
850
|
+
node.type === 'propertyOwner'
|
851
|
+
? 'none'
|
852
|
+
: isSelected
|
853
|
+
? '3px solid #ff6b35'
|
854
|
+
: isHighlighted
|
855
|
+
? '3px solid #4caf50'
|
856
|
+
: node.style?.border,
|
857
|
+
zIndex: isSelected ? 1000 : isHighlighted ? 100 : 1,
|
858
|
+
},
|
859
|
+
};
|
860
|
+
});
|
861
|
+
|
862
|
+
const highlightedEdgeIds = findRelevantEdges(highlightedNodeIds, edges);
|
863
|
+
|
864
|
+
const enhancedEdges = edges.map((edge) => {
|
865
|
+
const isHighlighted = highlightedEdgeIds.has(edge.id);
|
866
|
+
|
867
|
+
return {
|
868
|
+
...edge,
|
869
|
+
style: {
|
870
|
+
...edge.style,
|
871
|
+
strokeWidth: isHighlighted ? 4 : 2,
|
872
|
+
stroke: isHighlighted ? '#4caf50' : '#1976d2',
|
873
|
+
opacity: isHighlighted ? 1 : 0.6,
|
874
|
+
},
|
875
|
+
};
|
876
|
+
});
|
877
|
+
|
878
|
+
// DO NOT set selectedSourcePropertiesMap here as it causes infinite re-renders
|
879
|
+
// The map is already set in handlePropertyClick
|
880
|
+
return (
|
881
|
+
<div style={{ height: '100%', width: '100%', display: 'flex' }}>
|
882
|
+
<div style={{ flex: 1, height: '100%' }}>
|
883
|
+
<ReactFlowProvider>
|
884
|
+
<ReactFlow
|
885
|
+
nodes={enhancedNodes}
|
886
|
+
edges={enhancedEdges}
|
887
|
+
nodeTypes={PROPERTY_LINEAGE_NODE_TYPES}
|
888
|
+
defaultEdgeOptions={{ type: 'default' }}
|
889
|
+
defaultViewport={{ x: 0, y: 0, zoom: 0.6 }}
|
890
|
+
fitView={true}
|
891
|
+
fitViewOptions={{
|
892
|
+
padding: 0.2,
|
893
|
+
minZoom: 0.3,
|
894
|
+
maxZoom: 1.5,
|
895
|
+
}}
|
896
|
+
nodesDraggable={true}
|
897
|
+
onNodeClick={onNodeClick}
|
898
|
+
nodeExtent={[
|
899
|
+
[-1000, -1000],
|
900
|
+
[3000, 3000],
|
901
|
+
]}
|
902
|
+
key={`${lineageState.selectedProperty}-${sourcePropertiesMap.size}`}
|
903
|
+
>
|
904
|
+
<Background />
|
905
|
+
<MiniMap />
|
906
|
+
<Controls />
|
907
|
+
</ReactFlow>
|
908
|
+
</ReactFlowProvider>
|
909
|
+
</div>
|
910
|
+
|
911
|
+
{lineageState.selectedPropertyOwnerNode && (
|
912
|
+
<div style={{ width: '350px', borderLeft: '1px solid #ccc' }}>
|
913
|
+
<PropertyOwnerPanel
|
914
|
+
lineageState={lineageState}
|
915
|
+
selectedNodeId={lineageState.selectedPropertyOwnerNode}
|
916
|
+
/>
|
917
|
+
</div>
|
918
|
+
)}
|
919
|
+
</div>
|
920
|
+
);
|
921
|
+
},
|
922
|
+
);
|
923
|
+
|
407
924
|
// Graph Viewer Component
|
408
925
|
const LineageGraphViewer = observer(
|
409
926
|
(props: { nodes: ReactFlowNode[]; edges: ReactFlowEdge[] }) => {
|
@@ -435,12 +952,14 @@ const TAB_ORDER = [
|
|
435
952
|
LINEAGE_VIEW_MODE.DATABASE_LINEAGE,
|
436
953
|
LINEAGE_VIEW_MODE.CLASS_LINEAGE,
|
437
954
|
LINEAGE_VIEW_MODE.REPORT_LINEAGE,
|
955
|
+
LINEAGE_VIEW_MODE.PROPERTY_LINEAGE,
|
438
956
|
];
|
439
957
|
|
440
958
|
const TAB_LABELS: Record<LINEAGE_VIEW_MODE, string> = {
|
441
959
|
[LINEAGE_VIEW_MODE.CLASS_LINEAGE]: 'Class Lineage',
|
442
960
|
[LINEAGE_VIEW_MODE.DATABASE_LINEAGE]: 'Database Lineage',
|
443
961
|
[LINEAGE_VIEW_MODE.REPORT_LINEAGE]: 'Report Lineage',
|
962
|
+
[LINEAGE_VIEW_MODE.PROPERTY_LINEAGE]: 'Property Lineage',
|
444
963
|
};
|
445
964
|
|
446
965
|
const LineageTabSelector = observer((props: { lineageState: LineageState }) => {
|
@@ -471,7 +990,6 @@ const LineageViewerContent = observer(
|
|
471
990
|
const selectedTab = lineageState.selectedTab;
|
472
991
|
const lineageData = lineageState.lineageData;
|
473
992
|
|
474
|
-
// Prepare all three graphs
|
475
993
|
const classLineageFlow = convertGraphToFlow(lineageData?.classLineage);
|
476
994
|
const databaseLineageFlow = convertGraphToFlow(
|
477
995
|
lineageData?.databaseLineage,
|
@@ -479,6 +997,10 @@ const LineageViewerContent = observer(
|
|
479
997
|
const reportLineageFlow = convertReportLineageToFlow(
|
480
998
|
lineageData?.reportLineage,
|
481
999
|
);
|
1000
|
+
const propertyLineageFlow = convertPropertyLineageToFlow(
|
1001
|
+
lineageData?.propertyLineage,
|
1002
|
+
lineageState.selectedSourcePropertiesMap,
|
1003
|
+
);
|
482
1004
|
|
483
1005
|
return (
|
484
1006
|
<div
|
@@ -506,6 +1028,13 @@ const LineageViewerContent = observer(
|
|
506
1028
|
edges={reportLineageFlow.edges}
|
507
1029
|
/>
|
508
1030
|
)}
|
1031
|
+
{selectedTab === LINEAGE_VIEW_MODE.PROPERTY_LINEAGE && (
|
1032
|
+
<PropertyLineageGraphViewer
|
1033
|
+
lineageState={lineageState}
|
1034
|
+
nodes={propertyLineageFlow.nodes}
|
1035
|
+
edges={propertyLineageFlow.edges}
|
1036
|
+
/>
|
1037
|
+
)}
|
509
1038
|
</PanelContent>
|
510
1039
|
</div>
|
511
1040
|
</div>
|
@@ -520,6 +1049,7 @@ export const LineageViewer = observer(
|
|
520
1049
|
const closePlanViewer = (): void => {
|
521
1050
|
lineageState.setLineageData(undefined);
|
522
1051
|
lineageState.setSelectedTab(LINEAGE_VIEW_MODE.DATABASE_LINEAGE);
|
1052
|
+
lineageState.clearPropertySelections();
|
523
1053
|
};
|
524
1054
|
|
525
1055
|
useEffect(() => {
|