@truedat/dd 8.11.7 → 8.11.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 +3 -3
- package/src/components/LineageGraph.js +24 -2
- package/src/components/__tests__/LineageGraphLayout.spec.js +20 -1
- package/src/components/__tests__/LineageInspectorPanel.spec.js +9 -2
- package/src/styles/{lineageGraph.less → LineageGraph.less} +1 -32
- package/src/utils/__tests__/lineageLayout.spec.js +18 -0
- package/src/utils/__tests__/lineageTopology.spec.js +1 -1
- package/src/utils/lineageLayout.js +5 -2
- package/src/components/__tests__/LineageGraphInteractions.spec.js +0 -660
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/dd",
|
|
3
|
-
"version": "8.11.
|
|
3
|
+
"version": "8.11.9",
|
|
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.
|
|
57
|
+
"@truedat/test": "8.11.9",
|
|
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": "
|
|
93
|
+
"gitHead": "7ecccf4e5e9e4bf052cbe4414ce196751335a235"
|
|
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 =
|
|
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)
|
|
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(
|
|
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(() =>
|
|
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/
|
|
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
|
-
|
|
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 -
|
|
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 =
|
|
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(
|
|
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
|
-
});
|