@truedat/dd 8.12.8 → 8.12.10
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/package.json +4 -4
- package/src/components/Graphs.js +6 -2
- package/src/components/LineageGraph.js +104 -11
- package/src/components/LineageGraphToolbar.js +50 -49
- package/src/components/LineageModuleButton.js +35 -0
- package/src/components/__tests__/LineageGraphInteraction.spec.js +238 -0
- package/src/components/__tests__/LineageGraphToolbar.spec.js +13 -17
- package/src/styles/LineageGraph.less +26 -77
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/dd",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.10",
|
|
4
4
|
"description": "Truedat Web Data Dictionary",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
"lodash": "^4.17.21",
|
|
70
70
|
"lucide-react": "^1.41.0",
|
|
71
71
|
"match-sorter": "^8.0.1",
|
|
72
|
-
"moment": "^2.
|
|
72
|
+
"moment": "^2.31.0",
|
|
73
73
|
"path-to-regexp": "^8.2.0",
|
|
74
74
|
"prop-types": "^15.8.1",
|
|
75
|
-
"query-string": "^
|
|
75
|
+
"query-string": "^9.5.1",
|
|
76
76
|
"react": "^19.2.7",
|
|
77
77
|
"react-csv": "^2.2.2",
|
|
78
78
|
"react-dom": "^19.2.7",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"svg-pan-zoom": "^3.6.2",
|
|
91
91
|
"swr": "^2.3.3"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "ac5f172e0ff4c087ea040ae29d1f2ea822bcadb6"
|
|
94
94
|
}
|
package/src/components/Graphs.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
useRef,
|
|
6
6
|
useState,
|
|
7
7
|
} from "react";
|
|
8
|
-
import { useParams } from "react-router";
|
|
8
|
+
import { useLocation, useParams } from "react-router";
|
|
9
9
|
import { Segment } from "semantic-ui-react";
|
|
10
10
|
import LineageHeader from "./LineageHeader";
|
|
11
11
|
import LineageSelectionPanel from "./LineageSelectionPanel";
|
|
@@ -14,6 +14,7 @@ import LineageGraph from "./LineageGraph";
|
|
|
14
14
|
|
|
15
15
|
export const Graphs = () => {
|
|
16
16
|
const { id } = useParams();
|
|
17
|
+
const { state } = useLocation();
|
|
17
18
|
const canvasRef = useRef(null);
|
|
18
19
|
useLayoutEffect(() => {
|
|
19
20
|
const canvas = canvasRef.current;
|
|
@@ -125,7 +126,10 @@ export const Graphs = () => {
|
|
|
125
126
|
/>
|
|
126
127
|
<main className="lineage-view__main" ref={canvasRef}>
|
|
127
128
|
<GraphRedirector />
|
|
128
|
-
<LineageGraph
|
|
129
|
+
<LineageGraph
|
|
130
|
+
height="var(--td-lineage-canvas-height)"
|
|
131
|
+
initialViewState={state?.lineageViewState}
|
|
132
|
+
/>
|
|
129
133
|
</main>
|
|
130
134
|
</div>
|
|
131
135
|
</Segment>
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
legacyPathEdges,
|
|
65
65
|
} from "../utils/lineageGraph";
|
|
66
66
|
import LineageResourceNode from "./LineageResourceNode";
|
|
67
|
+
import LineageModuleButton from "./LineageModuleButton";
|
|
67
68
|
import LineageGraphToolbar, { visibleLegendKeys } from "./LineageGraphToolbar";
|
|
68
69
|
|
|
69
70
|
export {
|
|
@@ -135,6 +136,7 @@ export const LineageGraph = ({
|
|
|
135
136
|
simplified = false,
|
|
136
137
|
requestedNodeIds,
|
|
137
138
|
structureId,
|
|
139
|
+
initialViewState,
|
|
138
140
|
}) => {
|
|
139
141
|
const preparedTopology = useMemo(
|
|
140
142
|
() => prepareLineageTopology(topology),
|
|
@@ -180,6 +182,8 @@ export const LineageGraph = ({
|
|
|
180
182
|
const [hoveredNodeId, setHoveredNodeId] = useState(null);
|
|
181
183
|
const [hoveredEdgeEndpoints, setHoveredEdgeEndpoints] = useState(null);
|
|
182
184
|
const stageRef = useRef(null);
|
|
185
|
+
const activeSurfaceRef = useRef(null);
|
|
186
|
+
const restoredViewStateRef = useRef(null);
|
|
183
187
|
const graphReadyRef = useRef(false);
|
|
184
188
|
const [fittedGraphKey, setFittedGraphKey] = useState(null);
|
|
185
189
|
const defaultCollapseSignatureRef = useRef("");
|
|
@@ -1069,6 +1073,76 @@ export const LineageGraph = ({
|
|
|
1069
1073
|
() => new Map(searchableGraphNodes.map((node) => [node.id, node])),
|
|
1070
1074
|
[searchableGraphNodes],
|
|
1071
1075
|
);
|
|
1076
|
+
useEffect(() => {
|
|
1077
|
+
if (
|
|
1078
|
+
!initialViewState ||
|
|
1079
|
+
restoredViewStateRef.current === initialViewState ||
|
|
1080
|
+
loading ||
|
|
1081
|
+
!hasGraph ||
|
|
1082
|
+
String(initialViewState.graphId) !== String(graphId)
|
|
1083
|
+
)
|
|
1084
|
+
return;
|
|
1085
|
+
|
|
1086
|
+
restoredViewStateRef.current = initialViewState;
|
|
1087
|
+
setExpansionDepth(
|
|
1088
|
+
normalizedExpansionDepth(
|
|
1089
|
+
initialViewState.expansionDepth,
|
|
1090
|
+
effectiveDefaultDepth,
|
|
1091
|
+
effectiveMaxDepth,
|
|
1092
|
+
),
|
|
1093
|
+
);
|
|
1094
|
+
const groupIds = new Set(collapsibleGroupIds);
|
|
1095
|
+
setCollapsedGroupIds(
|
|
1096
|
+
new Set(
|
|
1097
|
+
(initialViewState.collapsedGroupIds || []).filter((id) =>
|
|
1098
|
+
groupIds.has(id),
|
|
1099
|
+
),
|
|
1100
|
+
),
|
|
1101
|
+
);
|
|
1102
|
+
setExpandedFieldNodeIds(
|
|
1103
|
+
new Set(
|
|
1104
|
+
(initialViewState.expandedFieldNodeIds || []).filter((id) =>
|
|
1105
|
+
searchableNodeById.has(id),
|
|
1106
|
+
),
|
|
1107
|
+
),
|
|
1108
|
+
);
|
|
1109
|
+
const node = searchableNodeById.get(initialViewState.selectedGraphNodeId);
|
|
1110
|
+
const selectedNode =
|
|
1111
|
+
node && selectedNodeFromGraphNode(node, searchableNodeById);
|
|
1112
|
+
const edge = renderedGraphEdges.find(
|
|
1113
|
+
({ id }) => id === initialViewState.selectedGraphEdgeId,
|
|
1114
|
+
);
|
|
1115
|
+
const context = selectedNode
|
|
1116
|
+
? { type: "node", node: selectedNode }
|
|
1117
|
+
: edge
|
|
1118
|
+
? lineageEdgeContext({
|
|
1119
|
+
edge: edge.data || {},
|
|
1120
|
+
id: edge.id,
|
|
1121
|
+
source: edge.source,
|
|
1122
|
+
target: edge.target,
|
|
1123
|
+
nodeById: searchableNodeById,
|
|
1124
|
+
})
|
|
1125
|
+
: null;
|
|
1126
|
+
setSelectedContext(context);
|
|
1127
|
+
setPinnedGraphNodeId(
|
|
1128
|
+
searchableNodeById.has(initialViewState.pinnedGraphNodeId)
|
|
1129
|
+
? initialViewState.pinnedGraphNodeId
|
|
1130
|
+
: null,
|
|
1131
|
+
);
|
|
1132
|
+
setActivePanel(context ? "inspector" : null);
|
|
1133
|
+
setFitViewRequestKey((key) => (key || 0) + 1);
|
|
1134
|
+
}, [
|
|
1135
|
+
initialViewState,
|
|
1136
|
+
loading,
|
|
1137
|
+
hasGraph,
|
|
1138
|
+
graphId,
|
|
1139
|
+
setExpansionDepth,
|
|
1140
|
+
effectiveDefaultDepth,
|
|
1141
|
+
effectiveMaxDepth,
|
|
1142
|
+
collapsibleGroupIds,
|
|
1143
|
+
searchableNodeById,
|
|
1144
|
+
renderedGraphEdges,
|
|
1145
|
+
]);
|
|
1072
1146
|
useEffect(() => {
|
|
1073
1147
|
if (!selectedGraphNodeId || searchableNodeById.has(selectedGraphNodeId))
|
|
1074
1148
|
return;
|
|
@@ -1108,14 +1182,9 @@ export const LineageGraph = ({
|
|
|
1108
1182
|
(event, node) => {
|
|
1109
1183
|
event?.preventDefault();
|
|
1110
1184
|
event?.stopPropagation();
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
!stageRef.current.contains(event.target)
|
|
1115
|
-
) {
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1185
|
+
activeSurfaceRef.current = event?.target?.closest?.(
|
|
1186
|
+
".td-graph-container",
|
|
1187
|
+
);
|
|
1119
1188
|
selectGraphNode(node);
|
|
1120
1189
|
},
|
|
1121
1190
|
[selectGraphNode],
|
|
@@ -1127,6 +1196,7 @@ export const LineageGraph = ({
|
|
|
1127
1196
|
const node = visibleNodeById.get(event.target.dataset.id);
|
|
1128
1197
|
|
|
1129
1198
|
if (!node || node.selectable === false) return;
|
|
1199
|
+
activeSurfaceRef.current = event.target.closest(".td-graph-container");
|
|
1130
1200
|
event.preventDefault();
|
|
1131
1201
|
event.stopPropagation();
|
|
1132
1202
|
selectGraphNode(node);
|
|
@@ -1196,7 +1266,10 @@ export const LineageGraph = ({
|
|
|
1196
1266
|
const closeActivePanel = useCallback(() => setActivePanel(null), []);
|
|
1197
1267
|
const focusContextButton = useCallback(() => {
|
|
1198
1268
|
requestAnimationFrame(() => {
|
|
1199
|
-
|
|
1269
|
+
(activeSurfaceRef.current?.isConnected
|
|
1270
|
+
? activeSurfaceRef.current
|
|
1271
|
+
: stageRef.current
|
|
1272
|
+
)
|
|
1200
1273
|
?.querySelector(".lineage-graph__dock-context-clear")
|
|
1201
1274
|
?.focus();
|
|
1202
1275
|
});
|
|
@@ -1250,9 +1323,13 @@ export const LineageGraph = ({
|
|
|
1250
1323
|
const closeOnEscape = (event) => {
|
|
1251
1324
|
if (event.key !== "Escape") return;
|
|
1252
1325
|
|
|
1253
|
-
if (pinnedGraphNodeId) return;
|
|
1326
|
+
if (pinnedGraphNodeId && !activePanel) return;
|
|
1254
1327
|
event.preventDefault();
|
|
1255
1328
|
event.stopPropagation();
|
|
1329
|
+
if (pinnedGraphNodeId) {
|
|
1330
|
+
setActivePanel(null);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1256
1333
|
if (selectedContext) {
|
|
1257
1334
|
clearSelectedContext();
|
|
1258
1335
|
return;
|
|
@@ -1408,7 +1485,22 @@ export const LineageGraph = ({
|
|
|
1408
1485
|
onNodeDoubleClick={handleNodeDoubleClick}
|
|
1409
1486
|
onPaneClick={handlePaneSelectionClear}
|
|
1410
1487
|
topControls={graphTopControls}
|
|
1411
|
-
afterControls={
|
|
1488
|
+
afterControls={
|
|
1489
|
+
simplified ? (
|
|
1490
|
+
<LineageModuleButton
|
|
1491
|
+
disabled={loading || !graphId}
|
|
1492
|
+
viewState={{
|
|
1493
|
+
graphId,
|
|
1494
|
+
selectedGraphNodeId,
|
|
1495
|
+
selectedGraphEdgeId,
|
|
1496
|
+
pinnedGraphNodeId,
|
|
1497
|
+
expansionDepth,
|
|
1498
|
+
collapsedGroupIds: [...effectiveCollapsedGroupIds],
|
|
1499
|
+
expandedFieldNodeIds: [...expandedFieldNodeIds],
|
|
1500
|
+
}}
|
|
1501
|
+
/>
|
|
1502
|
+
) : null
|
|
1503
|
+
}
|
|
1412
1504
|
bottomControls={false}
|
|
1413
1505
|
graphOptions={{
|
|
1414
1506
|
controlsOrientation: "vertical",
|
|
@@ -1441,6 +1533,7 @@ export const LineageGraph = ({
|
|
|
1441
1533
|
};
|
|
1442
1534
|
|
|
1443
1535
|
LineageGraph.propTypes = {
|
|
1536
|
+
initialViewState: PropTypes.object,
|
|
1444
1537
|
defaultExpansionDepth: PropTypes.number,
|
|
1445
1538
|
edges: PropTypes.array,
|
|
1446
1539
|
graphId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
@@ -87,7 +87,7 @@ DockIcon.propTypes = {
|
|
|
87
87
|
name: PropTypes.string,
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
const LineageToolButton = ({
|
|
90
|
+
export const LineageToolButton = ({
|
|
91
91
|
active = false,
|
|
92
92
|
children,
|
|
93
93
|
className,
|
|
@@ -552,8 +552,7 @@ export const LineageGraphToolbar = ({
|
|
|
552
552
|
const [depthPopoverOpen, setDepthPopoverOpen] = useState(false);
|
|
553
553
|
const [settingsPopoverOpen, setSettingsPopoverOpen] = useState(false);
|
|
554
554
|
const hasContext = Boolean(context);
|
|
555
|
-
const inspectorOpen =
|
|
556
|
-
!simplified && hasContext && activePanel === "inspector";
|
|
555
|
+
const inspectorOpen = hasContext && activePanel === "inspector";
|
|
557
556
|
const selectedLabel = contextLabel(context);
|
|
558
557
|
const selectedTypeLabel = hasContext
|
|
559
558
|
? contextTypeLabel(context, formatMessage)
|
|
@@ -697,54 +696,56 @@ export const LineageGraphToolbar = ({
|
|
|
697
696
|
size="mini"
|
|
698
697
|
tooltipPosition="left center"
|
|
699
698
|
/>
|
|
700
|
-
{collapsible
|
|
699
|
+
{collapsible ? (
|
|
701
700
|
<>
|
|
702
701
|
<span className="lineage-graph__dock-divider" aria-hidden="true" />
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
<
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
702
|
+
{!simplified ? (
|
|
703
|
+
<Popup
|
|
704
|
+
on="click"
|
|
705
|
+
onClose={() => setSettingsPopoverOpen(false)}
|
|
706
|
+
onOpen={() => setSettingsPopoverOpen(true)}
|
|
707
|
+
open={settingsPopoverOpen}
|
|
708
|
+
position="left center"
|
|
709
|
+
trigger={
|
|
710
|
+
<LineageToolButton
|
|
711
|
+
active={settingsPopoverOpen}
|
|
712
|
+
aria-expanded={settingsPopoverOpen}
|
|
713
|
+
aria-haspopup="dialog"
|
|
714
|
+
className="lineage-graph__dock-settings-button"
|
|
715
|
+
icon={
|
|
716
|
+
<Settings
|
|
717
|
+
size={DOCK_ICON_SIZE}
|
|
718
|
+
strokeWidth={DOCK_ICON_STROKE}
|
|
719
|
+
/>
|
|
720
|
+
}
|
|
721
|
+
label={settingsLabel}
|
|
722
|
+
showTooltip={false}
|
|
723
|
+
/>
|
|
724
|
+
}
|
|
725
|
+
content={
|
|
726
|
+
<div
|
|
727
|
+
aria-label={settingsLabel}
|
|
728
|
+
className="lineage-graph__settings-popover"
|
|
729
|
+
role="dialog"
|
|
730
|
+
>
|
|
731
|
+
<span className="lineage-graph__settings-popover-header">
|
|
732
|
+
{settingsLabel}
|
|
733
|
+
</span>
|
|
734
|
+
<label className="lineage-graph__settings-toggle">
|
|
735
|
+
<input
|
|
736
|
+
checked={doubleClickExpandsNodes}
|
|
737
|
+
onChange={(event) =>
|
|
738
|
+
onDoubleClickExpandsNodesChange?.(event.target.checked)
|
|
739
|
+
}
|
|
740
|
+
type="checkbox"
|
|
741
|
+
/>
|
|
742
|
+
<span>{doubleClickInfoLabel}</span>
|
|
743
|
+
</label>
|
|
744
|
+
</div>
|
|
745
|
+
}
|
|
746
|
+
className="lineage-graph__settings-popup"
|
|
747
|
+
/>
|
|
748
|
+
) : null}
|
|
748
749
|
<Popup
|
|
749
750
|
on="click"
|
|
750
751
|
onClose={() => setDepthPopoverOpen(false)}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { generatePath, useNavigate } from "react-router";
|
|
4
|
+
import { GRAPH } from "@truedat/core/routes";
|
|
5
|
+
import { useAuthorized } from "@truedat/core/hooks/useAuthorized";
|
|
6
|
+
import { LineageToolButton } from "./LineageGraphToolbar";
|
|
7
|
+
|
|
8
|
+
export default function LineageModuleButton({ disabled, viewState }) {
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const { formatMessage } = useIntl();
|
|
11
|
+
const authorized = useAuthorized("lineage");
|
|
12
|
+
|
|
13
|
+
if (!authorized) return null;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<LineageToolButton
|
|
17
|
+
disabled={disabled || !viewState.graphId}
|
|
18
|
+
icon="sitemap"
|
|
19
|
+
label={formatMessage({
|
|
20
|
+
id: "lineage.graph.controls.open_module",
|
|
21
|
+
defaultMessage: "View in Lineage module",
|
|
22
|
+
})}
|
|
23
|
+
onClick={() =>
|
|
24
|
+
navigate(generatePath(GRAPH, { id: viewState.graphId }), {
|
|
25
|
+
state: { lineageViewState: viewState },
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
LineageModuleButton.propTypes = {
|
|
33
|
+
disabled: PropTypes.bool,
|
|
34
|
+
viewState: PropTypes.object.isRequired,
|
|
35
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { fireEvent, screen, within } from "@testing-library/react";
|
|
2
|
+
import { Route, Routes, useLocation } from "react-router";
|
|
3
|
+
import { render } from "@truedat/test/render";
|
|
4
|
+
import { LineageGraph } from "../LineageGraph";
|
|
5
|
+
import { GraphLoader } from "../GraphLoader";
|
|
6
|
+
|
|
7
|
+
let mockGraphProps;
|
|
8
|
+
|
|
9
|
+
jest.mock("@truedat/core/components/Graph", () => {
|
|
10
|
+
const React = require("react");
|
|
11
|
+
const { createPortal } = require("react-dom");
|
|
12
|
+
return function Graph(props) {
|
|
13
|
+
mockGraphProps = props;
|
|
14
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
15
|
+
React.useEffect(
|
|
16
|
+
() => props.onInitialFitComplete(),
|
|
17
|
+
[props.onInitialFitComplete],
|
|
18
|
+
);
|
|
19
|
+
const canvas = (name) => (
|
|
20
|
+
<div className="td-graph-container" data-testid={name}>
|
|
21
|
+
{props.nodes
|
|
22
|
+
.filter((node) => !node.hidden)
|
|
23
|
+
.map((node) => (
|
|
24
|
+
<button
|
|
25
|
+
className="react-flow__node"
|
|
26
|
+
data-id={node.id}
|
|
27
|
+
key={node.id}
|
|
28
|
+
onClick={(event) => props.onNodeClick(event, node)}
|
|
29
|
+
>
|
|
30
|
+
{node.id}
|
|
31
|
+
</button>
|
|
32
|
+
))}
|
|
33
|
+
{props.topControls}
|
|
34
|
+
{props.afterControls}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
{canvas("normal")}
|
|
40
|
+
<button onClick={() => setExpanded(!expanded)}>Toggle expanded</button>
|
|
41
|
+
{expanded
|
|
42
|
+
? createPortal(canvas("expanded"), global.document.body)
|
|
43
|
+
: null}
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const props = {
|
|
50
|
+
graphId: "graph-1",
|
|
51
|
+
loading: false,
|
|
52
|
+
simplified: true,
|
|
53
|
+
cancelPoll: jest.fn(),
|
|
54
|
+
groups: [],
|
|
55
|
+
resources: [
|
|
56
|
+
{ id: "orders", name: "Orders", x: 0, y: 0, w: 288, h: 100 },
|
|
57
|
+
{ id: "report", name: "Report", x: 400, y: 0, w: 288, h: 100 },
|
|
58
|
+
],
|
|
59
|
+
edges: [{ source: "orders", target: "report" }],
|
|
60
|
+
};
|
|
61
|
+
const state = { authentication: { role: "admin" }, graph: { id: "graph-1" } };
|
|
62
|
+
const Location = () => {
|
|
63
|
+
const location = useLocation();
|
|
64
|
+
return <output data-testid="location">{JSON.stringify(location)}</output>;
|
|
65
|
+
};
|
|
66
|
+
const renderGraph = (extra = {}) =>
|
|
67
|
+
render(
|
|
68
|
+
<>
|
|
69
|
+
<LineageGraph {...props} {...extra} />
|
|
70
|
+
<Location />
|
|
71
|
+
</>,
|
|
72
|
+
{ state },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
it("selects successive nodes through a real portal and keeps dependencies active on return", () => {
|
|
76
|
+
renderGraph();
|
|
77
|
+
fireEvent.click(screen.getByText("Toggle expanded"));
|
|
78
|
+
const expanded = within(screen.getByTestId("expanded"));
|
|
79
|
+
fireEvent.click(
|
|
80
|
+
expanded.getByRole("button", { name: "orders", exact: true }),
|
|
81
|
+
);
|
|
82
|
+
expect(expanded.getByLabelText("Lineage inspector")).toHaveTextContent(
|
|
83
|
+
"Orders",
|
|
84
|
+
);
|
|
85
|
+
expect(mockGraphProps.edges.some((edge) => edge.data.animated)).toBe(true);
|
|
86
|
+
fireEvent.click(
|
|
87
|
+
expanded.getByRole("button", { name: "report", exact: true }),
|
|
88
|
+
);
|
|
89
|
+
expect(expanded.getByLabelText("Lineage inspector")).toHaveTextContent(
|
|
90
|
+
"Report",
|
|
91
|
+
);
|
|
92
|
+
fireEvent.click(screen.getByText("Toggle expanded"));
|
|
93
|
+
expect(screen.getByLabelText("Lineage inspector")).toHaveTextContent(
|
|
94
|
+
"Report",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("transfers serializable selection and expansion to the existing graph route", () => {
|
|
99
|
+
renderGraph();
|
|
100
|
+
fireEvent.click(screen.getByRole("button", { name: "orders", exact: true }));
|
|
101
|
+
fireEvent.click(
|
|
102
|
+
screen.getByRole("button", { name: "View in Lineage module" }),
|
|
103
|
+
);
|
|
104
|
+
const location = JSON.parse(screen.getByTestId("location").textContent);
|
|
105
|
+
expect(location.pathname).toBe("/graphs/graph-1");
|
|
106
|
+
expect(location.state.lineageViewState).toMatchObject({
|
|
107
|
+
graphId: "graph-1",
|
|
108
|
+
selectedGraphNodeId: "orders",
|
|
109
|
+
collapsedGroupIds: [],
|
|
110
|
+
expandedFieldNodeIds: [],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("restores once after loading and ignores missing node ids", () => {
|
|
115
|
+
const initialViewState = {
|
|
116
|
+
graphId: "graph-1",
|
|
117
|
+
selectedGraphNodeId: "orders",
|
|
118
|
+
pinnedGraphNodeId: "missing",
|
|
119
|
+
expansionDepth: 0,
|
|
120
|
+
collapsedGroupIds: ["missing"],
|
|
121
|
+
expandedFieldNodeIds: ["missing"],
|
|
122
|
+
};
|
|
123
|
+
const view = renderGraph({ loading: true, initialViewState });
|
|
124
|
+
expect(screen.queryByLabelText("Lineage inspector")).not.toBeInTheDocument();
|
|
125
|
+
view.rerender(
|
|
126
|
+
<LineageGraph {...props} initialViewState={initialViewState} />,
|
|
127
|
+
);
|
|
128
|
+
expect(screen.getByLabelText("Lineage inspector")).toHaveTextContent(
|
|
129
|
+
"Orders",
|
|
130
|
+
);
|
|
131
|
+
fireEvent.click(screen.getByRole("button", { name: "report", exact: true }));
|
|
132
|
+
expect(screen.getByLabelText("Lineage inspector")).toHaveTextContent(
|
|
133
|
+
"Report",
|
|
134
|
+
);
|
|
135
|
+
fireEvent.click(
|
|
136
|
+
screen.getByRole("button", { name: "View in Lineage module" }),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does not apply a snapshot to a different graph", () => {
|
|
141
|
+
renderGraph({
|
|
142
|
+
initialViewState: { graphId: "other", selectedGraphNodeId: "orders" },
|
|
143
|
+
});
|
|
144
|
+
expect(screen.queryByLabelText("Lineage inspector")).not.toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("selects portal nodes with the keyboard and clears context before closing", () => {
|
|
148
|
+
renderGraph();
|
|
149
|
+
fireEvent.click(screen.getByText("Toggle expanded"));
|
|
150
|
+
const node = within(screen.getByTestId("expanded")).getByRole("button", {
|
|
151
|
+
name: "orders",
|
|
152
|
+
exact: true,
|
|
153
|
+
});
|
|
154
|
+
fireEvent.keyDown(node, { key: "Enter" });
|
|
155
|
+
expect(
|
|
156
|
+
within(screen.getByTestId("expanded")).getByLabelText("Lineage inspector"),
|
|
157
|
+
).toBeInTheDocument();
|
|
158
|
+
fireEvent.keyDown(node, { key: "Escape" });
|
|
159
|
+
expect(screen.queryByLabelText("Lineage inspector")).not.toBeInTheDocument();
|
|
160
|
+
expect(screen.getByTestId("expanded")).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("restores groups, fields, depth and a pinned selection without reapplying defaults", () => {
|
|
164
|
+
renderGraph({
|
|
165
|
+
groups: [
|
|
166
|
+
{ id: "database", name: "Database", x: -20, y: -60, w: 800, h: 220 },
|
|
167
|
+
],
|
|
168
|
+
initialViewState: {
|
|
169
|
+
graphId: "graph-1",
|
|
170
|
+
selectedGraphNodeId: "orders",
|
|
171
|
+
pinnedGraphNodeId: "orders",
|
|
172
|
+
expansionDepth: 1,
|
|
173
|
+
collapsedGroupIds: [],
|
|
174
|
+
expandedFieldNodeIds: ["orders", "missing"],
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
expect(screen.getAllByRole("button", { name: "Unpin focus" })).toHaveLength(
|
|
178
|
+
2,
|
|
179
|
+
);
|
|
180
|
+
fireEvent.click(
|
|
181
|
+
screen.getByRole("button", { name: "View in Lineage module" }),
|
|
182
|
+
);
|
|
183
|
+
expect(
|
|
184
|
+
JSON.parse(screen.getByTestId("location").textContent).state
|
|
185
|
+
.lineageViewState,
|
|
186
|
+
).toMatchObject({
|
|
187
|
+
selectedGraphNodeId: "orders",
|
|
188
|
+
pinnedGraphNodeId: "orders",
|
|
189
|
+
expansionDepth: 1,
|
|
190
|
+
collapsedGroupIds: [],
|
|
191
|
+
expandedFieldNodeIds: ["orders"],
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("hides module navigation without lineage permission", () => {
|
|
196
|
+
render(<LineageGraph {...props} />, {
|
|
197
|
+
state: { authentication: { groups: [] } },
|
|
198
|
+
});
|
|
199
|
+
expect(
|
|
200
|
+
screen.queryByRole("button", { name: "View in Lineage module" }),
|
|
201
|
+
).not.toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("disables module navigation when the graph has no identifier", () => {
|
|
205
|
+
renderGraph({ graphId: null });
|
|
206
|
+
expect(
|
|
207
|
+
screen.getByRole("button", { name: "View in Lineage module" }),
|
|
208
|
+
).toBeDisabled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it.each([
|
|
212
|
+
[{ id: "graph-1" }, false],
|
|
213
|
+
[{}, true],
|
|
214
|
+
[{ id: "other" }, true],
|
|
215
|
+
])(
|
|
216
|
+
"loads the existing graph only when it is not already available: %j",
|
|
217
|
+
(graph, needsFetch) => {
|
|
218
|
+
const fetchGraph = jest.fn();
|
|
219
|
+
render(
|
|
220
|
+
<Routes>
|
|
221
|
+
<Route
|
|
222
|
+
path="/graphs/:id"
|
|
223
|
+
element={
|
|
224
|
+
<GraphLoader
|
|
225
|
+
graph={graph}
|
|
226
|
+
fetchGraph={fetchGraph}
|
|
227
|
+
clearGraph={jest.fn()}
|
|
228
|
+
cancelPoll={jest.fn()}
|
|
229
|
+
/>
|
|
230
|
+
}
|
|
231
|
+
/>
|
|
232
|
+
</Routes>,
|
|
233
|
+
{ routes: ["/graphs/graph-1"] },
|
|
234
|
+
);
|
|
235
|
+
if (needsFetch) expect(fetchGraph).toHaveBeenCalledWith({ id: "graph-1" });
|
|
236
|
+
else expect(fetchGraph).not.toHaveBeenCalled();
|
|
237
|
+
},
|
|
238
|
+
);
|
|
@@ -180,31 +180,27 @@ describe("<LineageGraphToolbar />", () => {
|
|
|
180
180
|
);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
it("
|
|
184
|
-
|
|
183
|
+
it("opens the inspector and depth controls in structure views", async () => {
|
|
184
|
+
renderToolbar({
|
|
185
185
|
activePanel: "inspector",
|
|
186
186
|
simplified: true,
|
|
187
|
+
maxExpansionDepth: 8,
|
|
187
188
|
});
|
|
188
|
-
|
|
189
|
-
const contextBar = rendered.container.querySelector(
|
|
190
|
-
".lineage-graph__dock-context-bar",
|
|
191
|
-
);
|
|
192
|
-
expect(contextBar).toHaveTextContent("Orders");
|
|
193
|
-
expect(contextBar).not.toHaveClass(
|
|
194
|
-
"lineage-graph__dock-context-bar--expanded",
|
|
195
|
-
);
|
|
189
|
+
expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
|
|
196
190
|
expect(
|
|
197
|
-
screen.
|
|
191
|
+
screen.queryByRole("button", { name: "Search nodes" }),
|
|
198
192
|
).not.toBeInTheDocument();
|
|
199
|
-
fireEvent.click(screen.getByText("Orders"));
|
|
200
|
-
expect(defaultProps.onPanelChange).not.toHaveBeenCalled();
|
|
201
193
|
expect(
|
|
202
|
-
screen.
|
|
194
|
+
screen.queryByRole("button", { name: "Graph settings" }),
|
|
203
195
|
).not.toBeInTheDocument();
|
|
204
|
-
|
|
205
|
-
screen.getByRole("button", { name: "
|
|
196
|
+
await act(async () =>
|
|
197
|
+
fireEvent.click(screen.getByRole("button", { name: "Visible levels" })),
|
|
206
198
|
);
|
|
207
|
-
|
|
199
|
+
const slider = screen.getByRole("slider", { name: "Visible levels" });
|
|
200
|
+
fireEvent.change(slider, { target: { value: "0" } });
|
|
201
|
+
expect(defaultProps.onExpansionDepthChange).toHaveBeenLastCalledWith("0");
|
|
202
|
+
fireEvent.change(slider, { target: { value: "8" } });
|
|
203
|
+
expect(defaultProps.onExpansionDepthChange).toHaveBeenLastCalledWith("8");
|
|
208
204
|
});
|
|
209
205
|
|
|
210
206
|
it("reuses and expands the minimized context bar with the inspector", () => {
|
|
@@ -1391,7 +1391,7 @@
|
|
|
1391
1391
|
|
|
1392
1392
|
// Size overlay panels against the canvas, including sidebar/page gutters.
|
|
1393
1393
|
.lineage-graph__stage-surface {
|
|
1394
|
-
container-type:
|
|
1394
|
+
container-type: size;
|
|
1395
1395
|
}
|
|
1396
1396
|
|
|
1397
1397
|
.lineage-graph__stage .react-flow,
|
|
@@ -2321,27 +2321,6 @@
|
|
|
2321
2321
|
.structure-lineage {
|
|
2322
2322
|
--td-structure-lineage-height: clamp(480px, calc(100vh - 260px), 82vh);
|
|
2323
2323
|
}
|
|
2324
|
-
.structure-lineage .td-graph-container .react-flow__controls.top.right {
|
|
2325
|
-
display: flex;
|
|
2326
|
-
width: max-content;
|
|
2327
|
-
align-items: center;
|
|
2328
|
-
flex-direction: row;
|
|
2329
|
-
flex-wrap: nowrap;
|
|
2330
|
-
top: 12px;
|
|
2331
|
-
right: 12px;
|
|
2332
|
-
left: auto;
|
|
2333
|
-
margin: 0;
|
|
2334
|
-
}
|
|
2335
|
-
.structure-lineage .td-graph-container .react-flow__controls.top.right > span {
|
|
2336
|
-
display: inline-flex;
|
|
2337
|
-
flex: 0 0 auto;
|
|
2338
|
-
}
|
|
2339
|
-
.structure-lineage .lineage-graph__legend-control {
|
|
2340
|
-
display: none !important;
|
|
2341
|
-
}
|
|
2342
|
-
.structure-lineage .lineage-graph__selection-control {
|
|
2343
|
-
top: 52px;
|
|
2344
|
-
}
|
|
2345
2324
|
.structure-lineage .lineage-graph__canvas,
|
|
2346
2325
|
.structure-lineage .lineage-graph__stage,
|
|
2347
2326
|
.structure-lineage .td-graph-container {
|
|
@@ -2384,6 +2363,9 @@
|
|
|
2384
2363
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.13);
|
|
2385
2364
|
overflow: visible;
|
|
2386
2365
|
backdrop-filter: blur(10px);
|
|
2366
|
+
max-height: calc(100cqh - 24px);
|
|
2367
|
+
flex-wrap: wrap;
|
|
2368
|
+
align-content: flex-end;
|
|
2387
2369
|
}
|
|
2388
2370
|
.lineage-graph__stage-surface
|
|
2389
2371
|
.td-graph-controls--vertical
|
|
@@ -2509,7 +2491,8 @@
|
|
|
2509
2491
|
align-self: flex-end;
|
|
2510
2492
|
flex: 0 0 34px;
|
|
2511
2493
|
}
|
|
2512
|
-
.lineage-graph__dock-context > .lineage-graph__dock-context-button
|
|
2494
|
+
.lineage-graph__dock-context > .lineage-graph__dock-context-button,
|
|
2495
|
+
.lineage-graph__dock-context > .lineage-graph__dock-tooltip-trigger {
|
|
2513
2496
|
position: absolute;
|
|
2514
2497
|
top: 0;
|
|
2515
2498
|
right: 0;
|
|
@@ -2522,6 +2505,7 @@
|
|
|
2522
2505
|
z-index: 472;
|
|
2523
2506
|
display: flex;
|
|
2524
2507
|
width: var(--td-lineage-context-collapsed-width);
|
|
2508
|
+
max-width: calc(100cqw - 28px);
|
|
2525
2509
|
height: var(--td-lineage-context-control-size);
|
|
2526
2510
|
align-items: flex-start;
|
|
2527
2511
|
border-radius: 7px;
|
|
@@ -2946,12 +2930,8 @@
|
|
|
2946
2930
|
right: calc(100% + 10px);
|
|
2947
2931
|
z-index: 470;
|
|
2948
2932
|
display: flex;
|
|
2949
|
-
width: min(336px, calc(
|
|
2950
|
-
max-height: min(
|
|
2951
|
-
580px,
|
|
2952
|
-
calc(100vh - 86px),
|
|
2953
|
-
calc(var(--td-graph-container-height, 640px) - 24px)
|
|
2954
|
-
);
|
|
2933
|
+
width: min(336px, calc(100cqw - 82px));
|
|
2934
|
+
max-height: min(580px, calc(100vh - 86px), calc(100cqh - 24px));
|
|
2955
2935
|
flex-direction: column;
|
|
2956
2936
|
border: 1px solid #d7dde6;
|
|
2957
2937
|
border-radius: 10px;
|
|
@@ -2977,10 +2957,7 @@
|
|
|
2977
2957
|
max-height: min(
|
|
2978
2958
|
calc(580px - var(--td-lineage-context-expanded-height)),
|
|
2979
2959
|
calc(100vh - 86px - var(--td-lineage-context-expanded-height)),
|
|
2980
|
-
calc(
|
|
2981
|
-
var(--td-graph-container-height, 640px) - 24px -
|
|
2982
|
-
var(--td-lineage-context-expanded-height)
|
|
2983
|
-
)
|
|
2960
|
+
calc(100cqh - 24px - var(--td-lineage-context-expanded-height))
|
|
2984
2961
|
);
|
|
2985
2962
|
border-top: 0;
|
|
2986
2963
|
border-radius: 0 0 10px 10px;
|
|
@@ -3604,7 +3581,7 @@
|
|
|
3604
3581
|
.lineage-graph__tool-panel,
|
|
3605
3582
|
.lineage-inspector {
|
|
3606
3583
|
width: min(360px, calc(100cqw - 72px));
|
|
3607
|
-
max-height: calc(100vh - 72px);
|
|
3584
|
+
max-height: min(calc(100vh - 72px), calc(100cqh - 24px));
|
|
3608
3585
|
}
|
|
3609
3586
|
.lineage-graph__stage-surface .lineage-inspector--body-only {
|
|
3610
3587
|
right: calc(100% + var(--td-lineage-inspector-toolbar-offset) - 1px);
|
|
@@ -3811,43 +3788,6 @@
|
|
|
3811
3788
|
}
|
|
3812
3789
|
}
|
|
3813
3790
|
|
|
3814
|
-
.ui.modal.td-graph-modal > .content {
|
|
3815
|
-
padding: 0;
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
|
-
.ui.fullscreen.modal.td-graph-modal--expanded {
|
|
3819
|
-
inset: 12px auto auto 12px !important;
|
|
3820
|
-
width: calc(100vw - 24px) !important;
|
|
3821
|
-
height: calc(100vh - 24px);
|
|
3822
|
-
margin: 0 !important;
|
|
3823
|
-
border: 1px solid #d7dde6;
|
|
3824
|
-
border-radius: 8px;
|
|
3825
|
-
overflow: visible;
|
|
3826
|
-
}
|
|
3827
|
-
|
|
3828
|
-
.ui.fullscreen.modal.td-graph-modal--expanded > .close {
|
|
3829
|
-
top: -14px;
|
|
3830
|
-
right: -14px;
|
|
3831
|
-
z-index: 20;
|
|
3832
|
-
width: 30px;
|
|
3833
|
-
height: 30px;
|
|
3834
|
-
margin: 0;
|
|
3835
|
-
border-radius: 8px;
|
|
3836
|
-
background: #ffffff;
|
|
3837
|
-
box-shadow: 0 8px 18px rgba(21, 26, 36, 0.18);
|
|
3838
|
-
}
|
|
3839
|
-
|
|
3840
|
-
.ui.modal.td-graph-modal--expanded > .content {
|
|
3841
|
-
height: 100%;
|
|
3842
|
-
border-radius: 8px;
|
|
3843
|
-
background: transparent;
|
|
3844
|
-
overflow: hidden;
|
|
3845
|
-
}
|
|
3846
|
-
|
|
3847
|
-
.ui.modal.td-graph-modal--expanded .td-graph-container {
|
|
3848
|
-
height: 100%;
|
|
3849
|
-
}
|
|
3850
|
-
|
|
3851
3791
|
.td-graph-edge-metadata-badge {
|
|
3852
3792
|
position: absolute;
|
|
3853
3793
|
pointer-events: auto;
|
|
@@ -3944,27 +3884,27 @@
|
|
|
3944
3884
|
);
|
|
3945
3885
|
}
|
|
3946
3886
|
|
|
3947
|
-
.lineage-
|
|
3887
|
+
.lineage-graph__stage-surface
|
|
3948
3888
|
.react-flow__node.td-lineage-group-node--entity-system
|
|
3949
3889
|
> .td-lineage-group-node,
|
|
3950
|
-
.lineage-
|
|
3890
|
+
.lineage-graph__stage-surface
|
|
3951
3891
|
.react-flow__node.td-lineage-resource-node--entity-system
|
|
3952
3892
|
> .td-lineage-resource-node {
|
|
3953
3893
|
border-color: #b3b3b3;
|
|
3954
3894
|
}
|
|
3955
3895
|
|
|
3956
3896
|
// Catalog state only changes borders and group headers, never box backgrounds.
|
|
3957
|
-
.lineage-
|
|
3897
|
+
.lineage-graph__stage-surface
|
|
3958
3898
|
.react-flow__node.td-lineage-catalog--absent
|
|
3959
3899
|
> :is(.td-lineage-group-node, .td-lineage-resource-node) {
|
|
3960
3900
|
border: 2px dashed #b3b3b3;
|
|
3961
3901
|
}
|
|
3962
|
-
.lineage-
|
|
3902
|
+
.lineage-graph__stage-surface
|
|
3963
3903
|
.react-flow__node.td-lineage-catalog--deleted
|
|
3964
3904
|
> :is(.td-lineage-group-node, .td-lineage-resource-node) {
|
|
3965
3905
|
border: 2px dashed #dc2626;
|
|
3966
3906
|
}
|
|
3967
|
-
.lineage-
|
|
3907
|
+
.lineage-graph__stage-surface
|
|
3968
3908
|
.react-flow__node.td-lineage-catalog--absent
|
|
3969
3909
|
> .td-lineage-group-node {
|
|
3970
3910
|
:is(.td-lineage-group-node__header, .td-lineage-group-node__absolute-header) {
|
|
@@ -3983,7 +3923,7 @@
|
|
|
3983
3923
|
}
|
|
3984
3924
|
|
|
3985
3925
|
// Only catalog-present SYSTEM headers use the dark blue treatment.
|
|
3986
|
-
.lineage-
|
|
3926
|
+
.lineage-graph__stage-surface {
|
|
3987
3927
|
--td-graph-bg: #eaeaea;
|
|
3988
3928
|
|
|
3989
3929
|
.react-flow__node :is(.td-lineage-group-node, .td-lineage-resource-node) {
|
|
@@ -4095,3 +4035,12 @@
|
|
|
4095
4035
|
.lineage-graph__tool-panel--legend .lineage-graph__legend-section:empty {
|
|
4096
4036
|
display: none;
|
|
4097
4037
|
}
|
|
4038
|
+
|
|
4039
|
+
.lineage-graph__tool-panel--legend {
|
|
4040
|
+
overflow-y: auto;
|
|
4041
|
+
overscroll-behavior: contain;
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
.lineage-graph__legend-section {
|
|
4045
|
+
flex-shrink: 0;
|
|
4046
|
+
}
|