@truedat/dd 8.12.7 → 8.12.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dd",
3
- "version": "8.12.7",
3
+ "version": "8.12.9",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -54,14 +54,14 @@
54
54
  "@testing-library/jest-dom": "^6.6.3",
55
55
  "@testing-library/react": "^16.3.0",
56
56
  "@testing-library/user-event": "^14.6.1",
57
- "@truedat/test": "8.12.7",
57
+ "@truedat/test": "8.12.8",
58
58
  "identity-obj-proxy": "^3.0.0",
59
59
  "jest": "^29.7.0",
60
60
  "redux-saga-test-plan": "^4.0.6"
61
61
  },
62
62
  "dependencies": {
63
63
  "@apollo/client": "^3.13.8",
64
- "axios": "^1.15.0",
64
+ "axios": "^1.20.0",
65
65
  "file-saver": "^2.0.5",
66
66
  "graphql": "^16.11.0",
67
67
  "is-hotkey": "^0.2.0",
@@ -90,5 +90,5 @@
90
90
  "svg-pan-zoom": "^3.6.2",
91
91
  "swr": "^2.3.3"
92
92
  },
93
- "gitHead": "dae06fd2f915163d5f1e7e3f26c414c8ee9bf055"
93
+ "gitHead": "daeb646a9077620db07b6a385b705fe8b8dd62ed"
94
94
  }
@@ -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 height="var(--td-lineage-canvas-height)" />
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
- if (
1112
- stageRef.current &&
1113
- event?.target &&
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
- stageRef.current
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={null}
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 && !simplified ? (
699
+ {collapsible ? (
701
700
  <>
702
701
  <span className="lineage-graph__dock-divider" aria-hidden="true" />
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
- />
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("keeps the selected name in the minimized banner in structure views", () => {
184
- const rendered = renderToolbar({
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.queryByLabelText("Lineage inspector"),
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.queryByLabelText("Lineage inspector"),
194
+ screen.queryByRole("button", { name: "Graph settings" }),
203
195
  ).not.toBeInTheDocument();
204
- fireEvent.click(
205
- screen.getByRole("button", { name: "Clear selection for Orders" }),
196
+ await act(async () =>
197
+ fireEvent.click(screen.getByRole("button", { name: "Visible levels" })),
206
198
  );
207
- expect(defaultProps.onClearContext).toHaveBeenCalledTimes(1);
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", () => {
@@ -31,7 +31,13 @@ exports[`<PendingStructureNoteRow /> matches the latest snapshot 1`] = `
31
31
  <td
32
32
  class=""
33
33
  >
34
- bar domain, baz domain
34
+ <span>
35
+ bar domain
36
+ </span>
37
+ ,
38
+ <span>
39
+ baz domain
40
+ </span>
35
41
  </td>
36
42
  <td
37
43
  class=""
@@ -135,7 +135,7 @@ exports[`<PendingStructureNotes /> matches the latest snapshot 1`] = `
135
135
  pendingStructureNotes.props.system
136
136
  </th>
137
137
  <th
138
- class="one wide disabled"
138
+ class="two wide disabled"
139
139
  >
140
140
  pendingStructureNotes.props.domain
141
141
  </th>
@@ -176,7 +176,9 @@ exports[`<PendingStructureNotes /> matches the latest snapshot 1`] = `
176
176
  <td
177
177
  class=""
178
178
  >
179
- bar domain
179
+ <span>
180
+ bar domain
181
+ </span>
180
182
  </td>
181
183
  <td
182
184
  class=""
@@ -215,7 +217,13 @@ exports[`<PendingStructureNotes /> matches the latest snapshot 1`] = `
215
217
  <td
216
218
  class=""
217
219
  >
218
- foo domain 1, foo domain 2
220
+ <span>
221
+ foo domain 1
222
+ </span>
223
+ ,
224
+ <span>
225
+ foo domain 2
226
+ </span>
219
227
  </td>
220
228
  <td
221
229
  class=""
@@ -27,7 +27,7 @@ exports[`<PendingStructureNotesTable /> matches the latest snapshot 1`] = `
27
27
  System
28
28
  </th>
29
29
  <th
30
- class="one wide disabled"
30
+ class="two wide disabled"
31
31
  >
32
32
  Domain
33
33
  </th>
@@ -68,7 +68,9 @@ exports[`<PendingStructureNotesTable /> matches the latest snapshot 1`] = `
68
68
  <td
69
69
  class=""
70
70
  >
71
- bar domain
71
+ <span>
72
+ bar domain
73
+ </span>
72
74
  </td>
73
75
  <td
74
76
  class=""
@@ -107,7 +109,13 @@ exports[`<PendingStructureNotesTable /> matches the latest snapshot 1`] = `
107
109
  <td
108
110
  class=""
109
111
  >
110
- foo domain 1, foo domain 2
112
+ <span>
113
+ foo domain 1
114
+ </span>
115
+ ,
116
+ <span>
117
+ foo domain 2
118
+ </span>
111
119
  </td>
112
120
  <td
113
121
  class=""
@@ -1,9 +1,11 @@
1
1
  import _ from "lodash/fp";
2
+ import { Fragment } from "react";
2
3
  import PropTypes from "prop-types";
3
4
  import { createSelector } from "reselect";
4
5
  import { Link } from "react-router";
5
6
  import { linkTo } from "@truedat/core/routes";
6
7
  import { FormattedMessage } from "react-intl";
8
+ import { DomainTooltip } from "@truedat/bg/taxonomy/components/DomainPath";
7
9
 
8
10
  const StructureLink = (structure) =>
9
11
  structure?.id ? (
@@ -16,7 +18,12 @@ const translateDecorator = (id) =>
16
18
  id ? <FormattedMessage id={id} defaultMessage={id} /> : null;
17
19
 
18
20
  const DomainDecorator = (domains) =>
19
- _.flow(_.map("name"), _.join(", "))(domains);
21
+ domains?.map((domain, index) => (
22
+ <Fragment key={domain.id ?? domain.name}>
23
+ {index > 0 && ", "}
24
+ <DomainTooltip domain={domain} />
25
+ </Fragment>
26
+ ));
20
27
 
21
28
  DomainDecorator.propTypes = {
22
29
  domains: PropTypes.array,
@@ -57,7 +64,7 @@ export const defaultStructureNoteColumns = [
57
64
  name: "domain",
58
65
  fieldSelector: _.pathOr([], "domains"),
59
66
  fieldDecorator: DomainDecorator,
60
- width: 1,
67
+ width: 2,
61
68
  },
62
69
  {
63
70
  name: "path",
@@ -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: inline-size;
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(100vw - 82px));
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-graph
3887
+ .lineage-graph__stage-surface
3948
3888
  .react-flow__node.td-lineage-group-node--entity-system
3949
3889
  > .td-lineage-group-node,
3950
- .lineage-graph
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-graph
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-graph
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-graph
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-graph {
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
+ }