@truedat/dd 8.11.7 → 8.11.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dd",
3
- "version": "8.11.7",
3
+ "version": "8.11.10",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -54,7 +54,7 @@
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.11.7",
57
+ "@truedat/test": "8.11.10",
58
58
  "identity-obj-proxy": "^3.0.0",
59
59
  "jest": "^29.7.0",
60
60
  "redux-saga-test-plan": "^4.0.6"
@@ -90,5 +90,5 @@
90
90
  "svg-pan-zoom": "^3.6.2",
91
91
  "swr": "^2.3.3"
92
92
  },
93
- "gitHead": "ae97c12c9013e8881f997e7e407402fe6b1eff0a"
93
+ "gitHead": "890ce64985a0ba9f76ff857f1e51dea9948aa3e7"
94
94
  }
@@ -81,7 +81,7 @@ const LINEAGE_SYSTEM_TYPE_PALETTE = [
81
81
  ];
82
82
  const LINEAGE_COLUMN_HEADER_HEIGHT = 42;
83
83
  const POSITIONED_GROUP_PADDING_X = 16;
84
- const POSITIONED_GROUP_PADDING_BOTTOM = 34;
84
+ const POSITIONED_GROUP_PADDING_BOTTOM = 12;
85
85
  const POSITIONED_GROUP_LABEL_GUTTER = 58;
86
86
  const POSITIONED_SIBLING_GAP = 14;
87
87
  const POSITIONED_LEAF_SIBLING_GAP = 6;
@@ -2040,7 +2040,29 @@ export const collapseLineageNodes = (
2040
2040
  !isRootLineageGroupNode(node) &&
2041
2041
  hiddenChildrenCount > 0;
2042
2042
 
2043
- if (!canCollapse) return node;
2043
+ if (!canCollapse) {
2044
+ if (
2045
+ node.type !== "lineageGroup" ||
2046
+ node.data?.variant !== "absolute" ||
2047
+ isRootLineageGroupNode(node) ||
2048
+ hiddenChildrenCount > 0 ||
2049
+ node.data?.collapsed
2050
+ )
2051
+ return node;
2052
+
2053
+ const height = 44;
2054
+ return {
2055
+ ...node,
2056
+ height,
2057
+ style: { ...node.style, height },
2058
+ data: {
2059
+ ...node.data,
2060
+ absoluteGeometry: node.data?.absoluteGeometry
2061
+ ? { ...node.data.absoluteGeometry, h: height }
2062
+ : node.data?.absoluteGeometry,
2063
+ },
2064
+ };
2065
+ }
2044
2066
 
2045
2067
  const collapsed = effectiveCollapsedIds.has(node.id);
2046
2068
  const hasCollapsedDescendantGroups = Array.from(
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  applyFieldHandlesToEdges,
3
+ collapseLineageNodes,
3
4
  bundleLineageEdges,
4
5
  lineageEdge,
5
6
  lineageCollapsedGroupIdsForDepth,
@@ -146,7 +147,7 @@ describe("layoutVisibleLineageGraph", () => {
146
147
  const laidGroup = absoluteGeometryFor(laid, "group");
147
148
  const laidLeaf = absoluteGeometryFor(laid, "leaf");
148
149
 
149
- expect(laidGroup.y + laidGroup.h - (laidLeaf.y + laidLeaf.h)).toBe(34);
150
+ expect(laidGroup.y + laidGroup.h - (laidLeaf.y + laidLeaf.h)).toBe(12);
150
151
  });
151
152
 
152
153
  it("orders impact chains from left to right", () => {
@@ -618,3 +619,21 @@ describe("positioned lineage layout", () => {
618
619
  });
619
620
  });
620
621
  });
622
+
623
+ it("compacts empty open groups while preserving collapsed group summaries", () => {
624
+ const empty = groupNode("empty", 0, 0, 288, 82);
625
+ const parent = groupNode("parent", 400, 0, 288, 200);
626
+ const child = resourceNode("child", parent, 0, 50, 200, 80);
627
+ const result = collapseLineageNodes(
628
+ [empty, parent, child],
629
+ new Set(["parent"]),
630
+ () => {},
631
+ );
632
+ const compact = result.find(({ id }) => id === "empty");
633
+ expect(compact.height).toBe(44);
634
+ expect(compact.style.height).toBe(44);
635
+ expect(compact.data.absoluteGeometry.h).toBe(44);
636
+ const collapsed = result.find(({ id }) => id === "parent");
637
+ expect(collapsed.height).toBe(82);
638
+ expect(collapsed.data.collapsed).toBe(true);
639
+ });
@@ -16,7 +16,14 @@ describe("<LineageInspectorPanel />", () => {
16
16
  const onMinimize = jest.fn();
17
17
  const onTogglePin = jest.fn();
18
18
 
19
- beforeEach(() => jest.clearAllMocks());
19
+ beforeEach(() => {
20
+ jest.useFakeTimers();
21
+ jest.clearAllMocks();
22
+ });
23
+
24
+ afterEach(() => {
25
+ jest.useRealTimers();
26
+ });
20
27
 
21
28
  it("shows real node facts, quality and conditional resource actions", () => {
22
29
  render(
@@ -325,7 +332,7 @@ describe("<LineageInspectorPanel />", () => {
325
332
 
326
333
  it("uses one motion token, reduced motion and the neutral detail hover", () => {
327
334
  const styles = fs.readFileSync(
328
- path.resolve(__dirname, "../../styles/lineageGraph.less"),
335
+ path.resolve(__dirname, "../../styles/LineageGraph.less"),
329
336
  "utf8",
330
337
  );
331
338
 
@@ -3680,38 +3680,7 @@
3680
3680
  }
3681
3681
 
3682
3682
  .td-graph-minimap-expand {
3683
- position: absolute;
3684
- bottom: 6px;
3685
- left: 6px;
3686
- z-index: 2;
3687
- display: inline-flex;
3688
- width: 22px;
3689
- height: 22px;
3690
- align-items: center;
3691
- justify-content: center;
3692
- padding: 0;
3693
- border: 1px solid rgba(15, 23, 42, 0.12);
3694
- border-radius: 5px;
3695
- background: rgba(255, 255, 255, 0.88);
3696
- color: #64748b;
3697
- cursor: pointer;
3698
- backdrop-filter: blur(6px);
3699
- -webkit-backdrop-filter: blur(6px);
3700
- transition:
3701
- background-color 120ms ease,
3702
- color 120ms ease,
3703
- border-color 120ms ease;
3704
- }
3705
-
3706
- .td-graph-minimap-expand:hover {
3707
- background: rgba(255, 255, 255, 0.98);
3708
- border-color: #002057;
3709
- color: #002057;
3710
- }
3711
-
3712
- .td-graph-minimap-expand:focus-visible {
3713
- outline: 2px solid #ed5c17;
3714
- outline-offset: 2px;
3683
+ display: none;
3715
3684
  }
3716
3685
 
3717
3686
  .td-graph-minimap-header {
@@ -232,3 +232,21 @@ describe("compound root columns", () => {
232
232
  expect(expanded.sink.x).toBe(expanded.global.x + expanded.global.w + 152);
233
233
  });
234
234
  });
235
+
236
+ it.each([undefined, "lineage-field"])(
237
+ "leaves only bottom padding after the last child (%s)",
238
+ (variant) => {
239
+ const nodes = [
240
+ { ...node("group"), type: "lineageGroup" },
241
+ {
242
+ ...node("child"),
243
+ parentId: "group",
244
+ data: { label: "child", variant },
245
+ },
246
+ ];
247
+ const result = geometry(freshLayout()({ nodes, edges: [] }));
248
+ expect(
249
+ result.group.y + result.group.h - result.child.y - result.child.h,
250
+ ).toBe(12);
251
+ },
252
+ );
@@ -125,7 +125,7 @@ describe("explicit lineage topology", () => {
125
125
  expect(box.x).toBeGreaterThanOrEqual(parent.x);
126
126
  expect(box.y).toBeGreaterThanOrEqual(parent.y + 58);
127
127
  expect(box.x + box.w).toBeLessThanOrEqual(parent.x + parent.w);
128
- expect(box.y + box.h).toBeLessThanOrEqual(parent.y + parent.h - 34);
128
+ expect(box.y + box.h).toBeLessThanOrEqual(parent.y + parent.h - 12);
129
129
  }
130
130
  laid
131
131
  .filter(
@@ -5,7 +5,7 @@ const NODE_GAP = 44;
5
5
  const FIELD_GAP = 4;
6
6
  const ROOT_BRANCH_GAP = 40;
7
7
  const GROUP_PADDING_X = 16;
8
- const GROUP_PADDING_BOTTOM = 34;
8
+ const GROUP_PADDING_BOTTOM = 12;
9
9
  const GROUP_HEADER_HEIGHT = 58;
10
10
  const MIN_GROUP_WIDTH = 240;
11
11
  const MIN_GROUP_HEIGHT = 82;
@@ -1008,7 +1008,10 @@ const compoundGeometry = (nodes, edges) => {
1008
1008
  return {
1009
1009
  geometries: outerAcc.geometries,
1010
1010
  x: outerAcc.x + maxWidth + COLUMN_GAP,
1011
- height: Math.max(outerAcc.height, endY),
1011
+ height: Math.max(
1012
+ outerAcc.height,
1013
+ endY - (fields ? FIELD_GAP : NODE_GAP),
1014
+ ),
1012
1015
  };
1013
1016
  },
1014
1017
  { geometries: new Map(), x: padding, height: header },
@@ -1,660 +0,0 @@
1
- /* eslint-disable react/prop-types -- Minimal render doubles exercise the real graph interactions. */
2
- import { act, fireEvent, screen } from "@testing-library/react";
3
- import { render } from "@truedat/test/render";
4
- import { LineageGraph } from "../LineageGraph";
5
- import * as lineageLayout from "../../utils/lineageLayout";
6
-
7
- const mockGraphRender = jest.fn();
8
-
9
- jest.mock("@truedat/core/components", () => ({
10
- Graph: (props) => {
11
- const { useContext } = require("react");
12
- const {
13
- EdgeHoverContext,
14
- } = require("@truedat/core/components/graph/ColoredEdge");
15
- const hover = useContext(EdgeHoverContext);
16
- mockGraphRender({ ...props, ...props.graphOptions });
17
-
18
- return (
19
- <div
20
- data-orientation={props.graphOptions.controlsOrientation}
21
- data-testid="mock-lineage-graph"
22
- >
23
- {props.topControls}
24
- {props.nodes.map((node) => (
25
- <button
26
- aria-label={`select-${node.id}`}
27
- className="react-flow__node"
28
- data-id={node.id}
29
- key={node.id}
30
- onMouseEnter={() => hover.setHoveredNodeId(node.id)}
31
- onMouseLeave={() => hover.setHoveredNodeId(null)}
32
- onClick={(event) => props.onNodeClick(event, node)}
33
- onDoubleClick={(event) => props.onNodeDoubleClick(event, node)}
34
- type="button"
35
- />
36
- ))}
37
- {props.edges.map((edge) =>
38
- edge.data?.onClick ? (
39
- <button
40
- aria-label={`select-edge-${edge.id}`}
41
- className="td-graph-edge-hit-area"
42
- key={edge.id}
43
- onClick={edge.data.onClick}
44
- type="button"
45
- />
46
- ) : null,
47
- )}
48
- <button
49
- aria-label="clear-canvas-selection"
50
- onClick={props.onPaneClick}
51
- type="button"
52
- />
53
- </div>
54
- );
55
- },
56
- }));
57
-
58
- const resources = [
59
- {
60
- external_id: "warehouse.sales.orders",
61
- fields: [],
62
- id: "orders",
63
- name: "Orders",
64
- schema: "sales",
65
- system: "Warehouse",
66
- system_group: "Oracle",
67
- },
68
- {
69
- external_id: "warehouse.sales.customers",
70
- fields: [],
71
- id: "customers",
72
- name: "Customers",
73
- schema: "sales",
74
- system: "Warehouse",
75
- system_group: "Oracle",
76
- },
77
- ];
78
-
79
- const renderGraph = () =>
80
- render(
81
- <LineageGraph
82
- cancelPoll={jest.fn()}
83
- edges={[]}
84
- groups={[]}
85
- resources={resources}
86
- />,
87
- { state: { graph: { id: "graph-1" } } },
88
- );
89
-
90
- describe("<LineageGraph /> toolbar interactions", () => {
91
- it("keeps the query origin independent from selection and clears selection", () => {
92
- render(
93
- <LineageGraph
94
- cancelPoll={jest.fn()}
95
- edges={[]}
96
- groups={[]}
97
- resources={resources}
98
- requestedNodeIds={["orders"]}
99
- />,
100
- { state: { graph: { id: "graph-1" } } },
101
- );
102
- const nodes = () => mockGraphRender.mock.calls.at(-1)[0].nodes;
103
- expect(
104
- nodes().find(({ id }) => id === "warehouse.sales.orders").className,
105
- ).toContain("lineage-node--origin");
106
- expect(nodes().some((node) => node.data.focusSelected)).toBe(false);
107
- expect(nodes().some((node) => node.data.focusDimmed)).toBe(false);
108
- fireEvent.click(
109
- screen.getByRole("button", { name: "select-warehouse.sales.customers" }),
110
- );
111
- expect(
112
- nodes().find(({ id }) => id === "warehouse.sales.orders").className,
113
- ).toContain("lineage-node--origin");
114
- expect(
115
- nodes().find(({ id }) => id === "warehouse.sales.customers").className,
116
- ).not.toContain("lineage-node--origin");
117
- expect(
118
- nodes().find(({ id }) => id === "warehouse.sales.customers").className,
119
- ).toContain("lineage-node--selected");
120
- fireEvent.click(
121
- screen.getByRole("button", { name: "clear-canvas-selection" }),
122
- );
123
- expect(
124
- nodes().some((node) => node.className.includes("lineage-node--selected")),
125
- ).toBe(false);
126
- expect(
127
- nodes().find(({ id }) => id === "warehouse.sales.orders").className,
128
- ).toContain("lineage-node--origin");
129
- });
130
-
131
- it.each(["Enter", " "])("opens the focused node inspector with %s", (key) => {
132
- renderGraph();
133
- const node = screen.getByRole("button", {
134
- name: "select-warehouse.sales.orders",
135
- });
136
- fireEvent.keyDown(node, { key });
137
- expect(screen.getByLabelText("Lineage inspector")).toHaveTextContent(
138
- "Orders",
139
- );
140
- expect(
141
- mockGraphRender.mock.calls
142
- .at(-1)[0]
143
- .nodes.find(({ id }) => id === "warehouse.sales.orders").ariaLabel,
144
- ).toBe("Orders");
145
- fireEvent.click(
146
- screen.getByRole("button", { name: "Clear selection for Orders" }),
147
- );
148
- fireEvent.keyDown(screen.getByRole("button", { name: "Search nodes" }), {
149
- key,
150
- });
151
- expect(
152
- screen.queryByLabelText("Lineage inspector"),
153
- ).not.toBeInTheDocument();
154
- });
155
-
156
- beforeEach(() => {
157
- jest.clearAllMocks();
158
- window.sessionStorage.clear();
159
- });
160
-
161
- it("ignores legacy stored depth and starts at the graph maximum below four", () => {
162
- window.sessionStorage.setItem("td.lineage.expansionDepth", "1");
163
-
164
- render(
165
- <LineageGraph
166
- cancelPoll={jest.fn()}
167
- edges={[]}
168
- graphId="depth-graph"
169
- groups={[
170
- {
171
- h: 300,
172
- id: "level-1",
173
- name: "Level 1",
174
- w: 300,
175
- x: 0,
176
- y: 0,
177
- z: 1,
178
- },
179
- {
180
- h: 220,
181
- id: "level-2",
182
- name: "Level 2",
183
- w: 220,
184
- x: 20,
185
- y: 50,
186
- z: 2,
187
- },
188
- ]}
189
- resources={[
190
- {
191
- h: 60,
192
- id: "leaf",
193
- name: "Leaf",
194
- w: 120,
195
- x: 40,
196
- y: 90,
197
- z: 3,
198
- },
199
- ]}
200
- />,
201
- { state: { graph: { id: "depth-graph" } } },
202
- );
203
-
204
- expect(
205
- screen.getByRole("button", { name: "Visible levels" }),
206
- ).toHaveTextContent("2");
207
- });
208
-
209
- it("opens the inspector on selection and closes it through clear selection", () => {
210
- renderGraph();
211
-
212
- expect(screen.getByTestId("mock-lineage-graph")).toHaveAttribute(
213
- "data-orientation",
214
- "vertical",
215
- );
216
- expect(
217
- screen.getByRole("button", { name: "Lineage legend" }),
218
- ).toBeInTheDocument();
219
-
220
- fireEvent.click(
221
- screen.getByRole("button", {
222
- name: "select-warehouse.sales.orders",
223
- }),
224
- );
225
-
226
- expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
227
-
228
- fireEvent.click(
229
- screen.getByRole("button", { name: "Clear selection for Orders" }),
230
- );
231
- expect(
232
- screen.queryByLabelText("Lineage inspector"),
233
- ).not.toBeInTheDocument();
234
-
235
- // Sin botón de reabrir en la barra: solo queda el clear.
236
- expect(
237
- screen.queryByRole("button", { name: "View information for Orders" }),
238
- ).not.toBeInTheDocument();
239
-
240
- fireEvent.click(
241
- screen.getByRole("button", { name: "clear-canvas-selection" }),
242
- );
243
- expect(
244
- screen.getByRole("button", { name: "Lineage legend" }),
245
- ).toBeInTheDocument();
246
-
247
- // Seleccionar otro nodo vuelve a abrir el inspector maximizado.
248
- fireEvent.click(
249
- screen.getByRole("button", {
250
- name: "select-warehouse.sales.customers",
251
- }),
252
- );
253
- expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
254
- expect(
255
- document.querySelector(".lineage-graph__dock-context-label"),
256
- ).toHaveTextContent("Customers");
257
-
258
- fireEvent.keyDown(document, { key: "Escape" });
259
- act(() => jest.runOnlyPendingTimers());
260
- expect(
261
- screen.queryByLabelText("Lineage inspector"),
262
- ).not.toBeInTheDocument();
263
- expect(
264
- screen.queryByRole("button", { name: "Clear selection for Customers" }),
265
- ).not.toBeInTheDocument();
266
- expect(
267
- screen.getByRole("button", { name: "Lineage legend" }),
268
- ).toBeInTheDocument();
269
- expect(
270
- mockGraphRender.mock.calls
271
- .at(-1)[0]
272
- .nodes.some((node) =>
273
- node.className.includes("lineage-node--selected"),
274
- ),
275
- ).toBe(false);
276
-
277
- fireEvent.click(
278
- screen.getByRole("button", { name: "clear-canvas-selection" }),
279
- );
280
- expect(
281
- screen.getByRole("button", { name: "Lineage legend" }),
282
- ).toBeInTheDocument();
283
- });
284
-
285
- it("debounces search and emits a repeatable focus request", () => {
286
- renderGraph();
287
- fireEvent.click(screen.getByRole("button", { name: "Search nodes" }));
288
- fireEvent.change(
289
- screen.getByRole("textbox", { name: "Search graph nodes" }),
290
- { target: { value: "warehouse.sales.orders" } },
291
- );
292
-
293
- expect(
294
- screen.queryByRole("option", { name: /Orders/ }),
295
- ).not.toBeInTheDocument();
296
-
297
- act(() => jest.advanceTimersByTime(299));
298
- expect(
299
- screen.queryByRole("option", { name: /Orders/ }),
300
- ).not.toBeInTheDocument();
301
-
302
- act(() => jest.advanceTimersByTime(1));
303
- fireEvent.click(screen.getByRole("option", { name: /Orders/ }));
304
-
305
- const firstFocusedProps = mockGraphRender.mock.calls.at(-1)[0];
306
-
307
- expect(firstFocusedProps.focusNodeId).toBe("warehouse.sales.orders");
308
- expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
309
-
310
- fireEvent.click(screen.getByRole("button", { name: "Search nodes" }));
311
- fireEvent.click(screen.getByRole("option", { name: /Orders/ }));
312
-
313
- const repeatedFocusProps = mockGraphRender.mock.calls.at(-1)[0];
314
-
315
- expect(repeatedFocusProps.focusNodeId).toBe("warehouse.sales.orders");
316
- expect(repeatedFocusProps.focusNodeRequestKey).toBeGreaterThan(
317
- firstFocusedProps.focusNodeRequestKey,
318
- );
319
- });
320
-
321
- it("re-centers the graph when the visible depth level changes", () => {
322
- render(
323
- <LineageGraph
324
- cancelPoll={jest.fn()}
325
- edges={[]}
326
- graphId="depth-graph"
327
- groups={[
328
- { h: 300, id: "level-1", name: "Level 1", w: 300, x: 0, y: 0, z: 1 },
329
- {
330
- h: 220,
331
- id: "level-2",
332
- name: "Level 2",
333
- w: 220,
334
- x: 20,
335
- y: 50,
336
- z: 2,
337
- },
338
- ]}
339
- resources={[
340
- { h: 60, id: "leaf", name: "Leaf", w: 120, x: 40, y: 90, z: 3 },
341
- ]}
342
- />,
343
- { state: { graph: { id: "depth-graph" } } },
344
- );
345
-
346
- const before = mockGraphRender.mock.calls.at(-1)[0];
347
-
348
- expect(before.fitViewRequestKey).toBeUndefined();
349
-
350
- fireEvent.click(screen.getByRole("button", { name: "Visible levels" }));
351
- fireEvent.change(screen.getByRole("slider", { name: "Visible levels" }), {
352
- target: { value: "0" },
353
- });
354
-
355
- const after = mockGraphRender.mock.calls.at(-1)[0];
356
-
357
- expect(after.fitViewRequestKey).toBeGreaterThan(
358
- before.fitViewRequestKey || 0,
359
- );
360
- });
361
-
362
- it("keeps the viewport and the acted node fixed when fields expand", () => {
363
- render(
364
- <LineageGraph
365
- cancelPoll={jest.fn()}
366
- edges={[]}
367
- groups={[]}
368
- resources={[
369
- {
370
- external_id: "warehouse.sales.expanded_orders",
371
- fields: Array.from({ length: 12 }, (_, index) => ({
372
- name: `field-${index}`,
373
- })),
374
- h: 82,
375
- id: "expanded-orders",
376
- name: "Expanded orders",
377
- w: 288,
378
- x: 420,
379
- y: 260,
380
- },
381
- ]}
382
- />,
383
- { state: { graph: { id: "graph-1" } } },
384
- );
385
-
386
- const initialProps = mockGraphRender.mock.calls.at(-1)[0];
387
- const initialNode = initialProps.nodes.find(
388
- ({ id }) => id === "warehouse.sales.expanded_orders",
389
- );
390
-
391
- expect(initialProps.fitViewOnLayoutChange).toBe(true);
392
- expect(initialProps.preserveUserViewportOnLayoutChange).toBe(true);
393
- expect(initialProps.fitViewRequestKey).toBeUndefined();
394
- expect(initialNode.data.onToggleFields).toEqual(expect.any(Function));
395
-
396
- act(() => initialNode.data.onToggleFields());
397
-
398
- const expandedProps = mockGraphRender.mock.calls.at(-1)[0];
399
- const expandedNode = expandedProps.nodes.find(
400
- ({ id }) => id === "warehouse.sales.expanded_orders",
401
- );
402
-
403
- expect(expandedNode.data.absoluteGeometry).toMatchObject({
404
- x: initialNode.data.absoluteGeometry.x,
405
- y: initialNode.data.absoluteGeometry.y,
406
- });
407
- expect(expandedNode.data.absoluteGeometry.h).toBeGreaterThan(
408
- initialNode.data.absoluteGeometry.h,
409
- );
410
- expect(expandedProps.fitViewOnLayoutChange).toBe(true);
411
- expect(expandedProps.preserveUserViewportOnLayoutChange).toBe(true);
412
- expect(expandedProps.fitViewRequestKey).toBeUndefined();
413
- expect(expandedProps.focusNodeId).toBeNull();
414
- });
415
-
416
- it("opens the inspector from another panel when a node is double-clicked", () => {
417
- renderGraph();
418
-
419
- fireEvent.click(
420
- screen.getByRole("button", {
421
- name: "select-warehouse.sales.orders",
422
- }),
423
- );
424
- fireEvent.click(screen.getByRole("button", { name: "Search nodes" }));
425
- fireEvent.doubleClick(
426
- screen.getByRole("button", {
427
- name: "select-warehouse.sales.customers",
428
- }),
429
- );
430
-
431
- expect(screen.getByLabelText("Lineage inspector")).toHaveTextContent(
432
- "Customers",
433
- );
434
- });
435
-
436
- it("keeps the inspector open and replaces its context on selection", () => {
437
- renderGraph();
438
-
439
- fireEvent.click(
440
- screen.getByRole("button", {
441
- name: "select-warehouse.sales.orders",
442
- }),
443
- );
444
- const customersNode = screen.getByRole("button", {
445
- name: "select-warehouse.sales.customers",
446
- });
447
-
448
- fireEvent.mouseDown(customersNode);
449
- fireEvent.click(customersNode);
450
-
451
- expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
452
- expect(
453
- document.querySelector(".lineage-graph__dock-context-label"),
454
- ).toHaveTextContent("Customers");
455
- expect(
456
- document.querySelector(".lineage-graph__dock-context-type"),
457
- ).toHaveTextContent("Table");
458
- expect(
459
- screen.getByRole("button", { name: "Clear selection for Customers" }),
460
- ).toBeInTheDocument();
461
- });
462
-
463
- it("opens the inspector when a connection with metadata is selected", () => {
464
- render(
465
- <LineageGraph
466
- cancelPoll={jest.fn()}
467
- edges={[
468
- {
469
- id: "orders-customers",
470
- metadata: { rule: "customer_id = id" },
471
- source: "warehouse.sales.orders",
472
- target: "warehouse.sales.customers",
473
- },
474
- ]}
475
- groups={[]}
476
- resources={resources}
477
- />,
478
- { state: { graph: { id: "graph-1" } } },
479
- );
480
-
481
- fireEvent.click(
482
- screen.getByRole("button", {
483
- name: "select-edge-orders-customers",
484
- }),
485
- );
486
-
487
- expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
488
- expect(
489
- document.querySelector(".lineage-graph__dock-context-label"),
490
- ).toHaveTextContent("Orders → Customers");
491
- expect(
492
- document.querySelector(".lineage-graph__dock-context-type"),
493
- ).toHaveTextContent("Connection metadata");
494
- expect(
495
- screen.getByRole("button", {
496
- name: "Clear selection for Orders → Customers",
497
- }),
498
- ).toBeInTheDocument();
499
- });
500
-
501
- it.skip("locks selection and panel controls while focus is pinned", () => {
502
- render(
503
- <LineageGraph
504
- cancelPoll={jest.fn()}
505
- edges={[
506
- {
507
- id: "orders-customers",
508
- metadata: { rule: "customer_id = id" },
509
- source: "warehouse.sales.orders",
510
- target: "warehouse.sales.customers",
511
- },
512
- ]}
513
- groups={[]}
514
- resources={resources}
515
- />,
516
- { state: { graph: { id: "graph-1" } } },
517
- );
518
-
519
- fireEvent.click(
520
- screen.getByRole("button", { name: "select-warehouse.sales.orders" }),
521
- );
522
- fireEvent.click(screen.getByRole("button", { name: "Pin focus" }));
523
- fireEvent.click(
524
- screen.getByRole("button", {
525
- name: "select-warehouse.sales.customers",
526
- }),
527
- );
528
- fireEvent.click(
529
- screen.getByRole("button", { name: "select-edge-orders-customers" }),
530
- );
531
- fireEvent.click(
532
- screen.getByRole("button", { name: "clear-canvas-selection" }),
533
- );
534
- fireEvent.mouseDown(document.body);
535
- fireEvent.keyDown(document, { key: "Escape" });
536
-
537
- expect(screen.getByLabelText("Lineage inspector")).toHaveTextContent(
538
- "Orders",
539
- );
540
- expect(screen.getByRole("button", { name: "Search nodes" })).toBeDisabled();
541
- expect(
542
- mockGraphRender.mock.calls
543
- .at(-1)[0]
544
- .nodes.find(({ id }) => id === "warehouse.sales.orders").data
545
- .focusSelected,
546
- ).toBe(true);
547
-
548
- expect(
549
- screen
550
- .getAllByRole("button", { name: "Unpin focus" })
551
- .every((button) => !button.disabled),
552
- ).toBe(true);
553
- expect(
554
- screen
555
- .getAllByRole("button", { name: "Unpin focus" })[1]
556
- .closest(".lineage-graph__dock-context"),
557
- ).toHaveClass("lineage-graph__dock-context--pinned");
558
-
559
- fireEvent.click(screen.getAllByRole("button", { name: "Unpin focus" })[0]);
560
- expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
561
- expect(
562
- screen.queryByRole("button", { name: "View information for Orders" }),
563
- ).not.toBeInTheDocument();
564
- fireEvent.click(
565
- screen.getByRole("button", {
566
- name: "select-warehouse.sales.customers",
567
- }),
568
- );
569
- expect(screen.getByLabelText("Lineage inspector")).toHaveTextContent(
570
- "Customers",
571
- );
572
-
573
- fireEvent.click(screen.getByRole("button", { name: "Pin focus" }));
574
- fireEvent.click(
575
- screen.getByRole("button", {
576
- name: "Clear selection for Customers",
577
- }),
578
- );
579
- expect(
580
- screen.getByRole("button", { name: "Lineage legend" }),
581
- ).toBeInTheDocument();
582
- });
583
- });
584
-
585
- it("projects multiple hidden query origins to their nearest visible container", () => {
586
- render(
587
- <LineageGraph
588
- cancelPoll={jest.fn()}
589
- edges={[]}
590
- graphId="origin-hierarchy"
591
- groups={[
592
- { h: 400, id: "system", name: "System", w: 400, x: 0, y: 0, z: 1 },
593
- { h: 300, id: "table", name: "Table", w: 300, x: 20, y: 50, z: 2 },
594
- ]}
595
- resources={[
596
- { h: 32, id: "field-a", name: "Field A", w: 200, x: 40, y: 90, z: 3 },
597
- { h: 32, id: "field-b", name: "Field B", w: 200, x: 40, y: 130, z: 3 },
598
- ]}
599
- requestedNodeIds={["field-a", "field-b"]}
600
- />,
601
- { state: { graph: { id: "origin-hierarchy" } } },
602
- );
603
- const nodes = () => mockGraphRender.mock.calls.at(-1)[0].nodes;
604
- expect(
605
- nodes().filter(({ data }) => data.queryOrigin === "direct"),
606
- ).toHaveLength(2);
607
- const table = nodes().find(({ id }) => id === "table");
608
- act(() => table.data.onToggleCollapse());
609
- expect(nodes().find(({ id }) => id === "table").data.queryOrigin).toBe(
610
- "contained",
611
- );
612
- expect(
613
- nodes().find(({ id }) => id === "system").data.queryOrigin,
614
- ).toBeUndefined();
615
- expect(nodes().some(({ id }) => id === "field-a")).toBe(false);
616
- act(() =>
617
- nodes()
618
- .find(({ id }) => id === "system")
619
- .data.onToggleCollapse(),
620
- );
621
- expect(nodes().find(({ id }) => id === "system").data.queryOrigin).toBe(
622
- "contained",
623
- );
624
- act(() =>
625
- nodes()
626
- .find(({ id }) => id === "system")
627
- .data.onToggleCollapse(),
628
- );
629
- act(() =>
630
- nodes()
631
- .find(({ id }) => id === "table")
632
- .data.onToggleCollapse(),
633
- );
634
- expect(
635
- nodes().filter(({ data }) => data.queryOrigin === "direct"),
636
- ).toHaveLength(2);
637
- });
638
-
639
- it("does not recompute topology or geometry on selection or hover", () => {
640
- const layout = jest.spyOn(lineageLayout, "layoutVisibleLineageGraph");
641
- renderGraph();
642
- const callCount = layout.mock.calls.length;
643
- const before = mockGraphRender.mock.calls
644
- .at(-1)[0]
645
- .nodes.map(({ position }) => position);
646
- fireEvent.click(
647
- screen.getByRole("button", { name: "select-warehouse.sales.orders" }),
648
- );
649
- fireEvent.mouseEnter(
650
- screen.getByRole("button", { name: "select-warehouse.sales.customers" }),
651
- );
652
- fireEvent.mouseLeave(
653
- screen.getByRole("button", { name: "select-warehouse.sales.customers" }),
654
- );
655
- expect(layout).toHaveBeenCalledTimes(callCount);
656
- expect(
657
- mockGraphRender.mock.calls.at(-1)[0].nodes.map(({ position }) => position),
658
- ).toEqual(before);
659
- layout.mockRestore();
660
- });