@truedat/core 8.14.0 → 8.14.2
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/TrueModal.js +40 -2
- package/src/components/__tests__/ColoredEdge.spec.js +216 -0
- package/src/components/__tests__/LineageGroupNode.spec.js +59 -0
- package/src/components/__tests__/TrueModal.spec.js +24 -1
- package/src/components/graph/ColoredEdge.js +153 -16
- package/src/components/graph/LineageGroupNode.js +41 -0
- package/src/styles/LineageGroupNode.less +39 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/core",
|
|
3
|
-
"version": "8.14.
|
|
3
|
+
"version": "8.14.2",
|
|
4
4
|
"description": "Truedat Web Core",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@testing-library/jest-dom": "^6.6.3",
|
|
58
58
|
"@testing-library/react": "^16.3.0",
|
|
59
59
|
"@testing-library/user-event": "^14.6.1",
|
|
60
|
-
"@truedat/test": "8.14.
|
|
60
|
+
"@truedat/test": "8.14.2",
|
|
61
61
|
"identity-obj-proxy": "^3.0.0",
|
|
62
62
|
"jest": "^29.7.0",
|
|
63
63
|
"redux-saga-test-plan": "^4.0.6"
|
|
@@ -99,5 +99,5 @@
|
|
|
99
99
|
"swr": "^2.3.3",
|
|
100
100
|
"turndown": "^7.2.2"
|
|
101
101
|
},
|
|
102
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "3814d239579315d817ba0e3467771e6a128e5788"
|
|
103
103
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useId } from "react";
|
|
1
|
+
import { useCallback, useId, useRef } from "react";
|
|
2
2
|
import PropTypes from "prop-types";
|
|
3
3
|
import { useIntl, FormattedMessage } from "react-intl";
|
|
4
4
|
import { Button, Icon, Modal } from "semantic-ui-react";
|
|
@@ -26,9 +26,44 @@ export const TrueModal = ({
|
|
|
26
26
|
onCancel,
|
|
27
27
|
onCreate,
|
|
28
28
|
className,
|
|
29
|
+
closeDisabled = false,
|
|
29
30
|
...modalProps
|
|
30
31
|
}) => {
|
|
31
32
|
const titleId = useId();
|
|
33
|
+
const headingRef = useRef(null);
|
|
34
|
+
const setHeading = useCallback((node) => {
|
|
35
|
+
headingRef.current = node;
|
|
36
|
+
if (!node) return undefined;
|
|
37
|
+
const frame = requestAnimationFrame(() => {
|
|
38
|
+
node.closest(".ui.modal")?.querySelector(".true-modal-close")?.focus();
|
|
39
|
+
});
|
|
40
|
+
return () => cancelAnimationFrame(frame);
|
|
41
|
+
}, []);
|
|
42
|
+
const handleKeyDown = (event) => {
|
|
43
|
+
modalProps.onKeyDown?.(event);
|
|
44
|
+
if (event.key !== "Tab" || event.defaultPrevented) return;
|
|
45
|
+
const dialog = headingRef.current?.closest(".ui.modal");
|
|
46
|
+
const controls = Array.from(
|
|
47
|
+
dialog?.querySelectorAll(
|
|
48
|
+
'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex="0"]',
|
|
49
|
+
) || [],
|
|
50
|
+
).filter(
|
|
51
|
+
(node) =>
|
|
52
|
+
node.getClientRects().length &&
|
|
53
|
+
!node.closest("[aria-disabled=true], fieldset[disabled]"),
|
|
54
|
+
);
|
|
55
|
+
const first = controls[0];
|
|
56
|
+
const last = controls[controls.length - 1];
|
|
57
|
+
if (
|
|
58
|
+
first &&
|
|
59
|
+
(!dialog.contains(document.activeElement) ||
|
|
60
|
+
(event.shiftKey && document.activeElement === first) ||
|
|
61
|
+
(!event.shiftKey && document.activeElement === last))
|
|
62
|
+
) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
(event.shiftKey ? last : first).focus();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
32
67
|
const { formatMessage } = useIntl();
|
|
33
68
|
const defaultActions = _.compact([
|
|
34
69
|
onCancel && {
|
|
@@ -50,6 +85,7 @@ export const TrueModal = ({
|
|
|
50
85
|
<Modal
|
|
51
86
|
{..._.omit(["header", "content", "closeIcon"])(modalProps)}
|
|
52
87
|
className={_.compact(["true-modal", className]).join(" ")}
|
|
88
|
+
onKeyDown={handleKeyDown}
|
|
53
89
|
role="dialog"
|
|
54
90
|
aria-labelledby={
|
|
55
91
|
modalProps["aria-labelledby"] ||
|
|
@@ -58,6 +94,7 @@ export const TrueModal = ({
|
|
|
58
94
|
closeIcon={
|
|
59
95
|
<button
|
|
60
96
|
type="button"
|
|
97
|
+
disabled={closeDisabled}
|
|
61
98
|
className="true-modal-close"
|
|
62
99
|
style={{ color: headerTextColor }}
|
|
63
100
|
aria-label={formatMessage({
|
|
@@ -71,7 +108,7 @@ export const TrueModal = ({
|
|
|
71
108
|
>
|
|
72
109
|
<Modal.Header style={{ background: headerColor, color: headerTextColor }}>
|
|
73
110
|
{icon ? <Icon name={icon} size="large" /> : null}
|
|
74
|
-
<div className="true-modal-heading">
|
|
111
|
+
<div ref={setHeading} className="true-modal-heading">
|
|
75
112
|
<span id={titleId}>{title}</span>
|
|
76
113
|
{subtitle != null ? (
|
|
77
114
|
<p className="true-modal-subtitle">{subtitle}</p>
|
|
@@ -132,6 +169,7 @@ TrueModal.propTypes = {
|
|
|
132
169
|
onCancel: PropTypes.func,
|
|
133
170
|
onCreate: PropTypes.func,
|
|
134
171
|
className: PropTypes.string,
|
|
172
|
+
closeDisabled: PropTypes.bool,
|
|
135
173
|
};
|
|
136
174
|
|
|
137
175
|
export default TrueModal;
|
|
@@ -5,6 +5,7 @@ import ColoredEdge, {
|
|
|
5
5
|
edgeAnimationDuration,
|
|
6
6
|
getClassicArrowPoints,
|
|
7
7
|
getFlexibleEdgePath,
|
|
8
|
+
isCompactInternalEdgeVisible,
|
|
8
9
|
} from "../graph/ColoredEdge";
|
|
9
10
|
|
|
10
11
|
jest.mock("@xyflow/react", () => ({
|
|
@@ -291,3 +292,218 @@ describe("edgeAnimationDuration", () => {
|
|
|
291
292
|
},
|
|
292
293
|
);
|
|
293
294
|
});
|
|
295
|
+
|
|
296
|
+
describe("guided internal curves", () => {
|
|
297
|
+
const props = {
|
|
298
|
+
id: "internal",
|
|
299
|
+
source: "a",
|
|
300
|
+
target: "b",
|
|
301
|
+
sourceX: 360,
|
|
302
|
+
sourceY: 80,
|
|
303
|
+
targetX: 512,
|
|
304
|
+
targetY: 160,
|
|
305
|
+
sourcePosition: "right",
|
|
306
|
+
targetPosition: "left",
|
|
307
|
+
markerEnd: { type: "arrow" },
|
|
308
|
+
};
|
|
309
|
+
const data = {
|
|
310
|
+
internalOwnerId: "g",
|
|
311
|
+
edgePath: "guided-bezier",
|
|
312
|
+
dependencyLayout: "compact",
|
|
313
|
+
lockSourcePort: true,
|
|
314
|
+
lockTargetPort: true,
|
|
315
|
+
metadata: { relation: "depends_on" },
|
|
316
|
+
routeGuide: {
|
|
317
|
+
kind: "corridor",
|
|
318
|
+
laneX: 420,
|
|
319
|
+
sourceStubX: 384,
|
|
320
|
+
targetStubX: 488,
|
|
321
|
+
labelPoint: { x: 420, y: 120 },
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
it("hides all interaction when unrelated and reveals the same curve for the active node", () => {
|
|
325
|
+
const onClick = jest.fn();
|
|
326
|
+
const view = render(
|
|
327
|
+
<EdgeHoverContext.Provider value={{ compactReferenceNodeId: "other" }}>
|
|
328
|
+
<svg>
|
|
329
|
+
<ColoredEdge {...props} data={{ ...data, onClick }} />
|
|
330
|
+
</svg>
|
|
331
|
+
</EdgeHoverContext.Provider>,
|
|
332
|
+
);
|
|
333
|
+
const path = view.container.querySelector("path#internal");
|
|
334
|
+
const d = path.getAttribute("d");
|
|
335
|
+
expect(d).toContain(" C ");
|
|
336
|
+
expect(path.closest("g")).toHaveStyle({ display: "none" });
|
|
337
|
+
expect(view.container.querySelector(".td-graph-edge-hit-area")).toBeNull();
|
|
338
|
+
expect(
|
|
339
|
+
view.container.querySelector(".td-graph-edge-metadata-badge"),
|
|
340
|
+
).toBeNull();
|
|
341
|
+
view.rerender(
|
|
342
|
+
<EdgeHoverContext.Provider value={{ compactReferenceNodeId: "a" }}>
|
|
343
|
+
<svg>
|
|
344
|
+
<ColoredEdge {...props} data={{ ...data, onClick }} />
|
|
345
|
+
</svg>
|
|
346
|
+
</EdgeHoverContext.Provider>,
|
|
347
|
+
);
|
|
348
|
+
expect(
|
|
349
|
+
view.container.querySelector("path#internal").getAttribute("d"),
|
|
350
|
+
).toBe(d);
|
|
351
|
+
const hit = view.container.querySelector(".td-graph-edge-hit-area");
|
|
352
|
+
expect(hit.getAttribute("d")).toBe(d);
|
|
353
|
+
fireEvent.click(hit);
|
|
354
|
+
expect(onClick).toHaveBeenCalledWith(data.metadata);
|
|
355
|
+
});
|
|
356
|
+
it("reveals indirect focused dependencies and hides them when selection clears", () => {
|
|
357
|
+
const view = render(
|
|
358
|
+
<EdgeHoverContext.Provider value={{ compactReferenceNodeId: "upstream" }}>
|
|
359
|
+
<svg>
|
|
360
|
+
<ColoredEdge {...props} data={{ ...data, focusFocused: true }} />
|
|
361
|
+
</svg>
|
|
362
|
+
</EdgeHoverContext.Provider>,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
expect(
|
|
366
|
+
view.container.querySelector("path#internal").closest("g"),
|
|
367
|
+
).not.toHaveStyle({ display: "none" });
|
|
368
|
+
expect(
|
|
369
|
+
view.container.querySelector(".td-graph-edge-hit-area"),
|
|
370
|
+
).not.toBeNull();
|
|
371
|
+
|
|
372
|
+
view.rerender(
|
|
373
|
+
<EdgeHoverContext.Provider value={{}}>
|
|
374
|
+
<svg>
|
|
375
|
+
<ColoredEdge {...props} data={data} />
|
|
376
|
+
</svg>
|
|
377
|
+
</EdgeHoverContext.Provider>,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(
|
|
381
|
+
view.container.querySelector("path#internal").closest("g"),
|
|
382
|
+
).toHaveStyle({ display: "none" });
|
|
383
|
+
expect(view.container.querySelector(".td-graph-edge-hit-area")).toBeNull();
|
|
384
|
+
});
|
|
385
|
+
it.each([
|
|
386
|
+
["hover", { hoveredNodeId: "a" }],
|
|
387
|
+
["keyboard focus", { keyboardFocusedNodeId: "a" }],
|
|
388
|
+
])(
|
|
389
|
+
"reveals the %s node dependencies while another node remains selected",
|
|
390
|
+
(_interaction, activeContext) => {
|
|
391
|
+
const view = render(
|
|
392
|
+
<EdgeHoverContext.Provider
|
|
393
|
+
value={{ compactReferenceNodeId: "selected-node", ...activeContext }}
|
|
394
|
+
>
|
|
395
|
+
<svg>
|
|
396
|
+
<ColoredEdge {...props} data={data} />
|
|
397
|
+
</svg>
|
|
398
|
+
</EdgeHoverContext.Provider>,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(
|
|
402
|
+
view.container.querySelector("path#internal").closest("g"),
|
|
403
|
+
).not.toHaveStyle({ display: "none" });
|
|
404
|
+
expect(
|
|
405
|
+
view.container.querySelector(".td-graph-edge-hit-area"),
|
|
406
|
+
).not.toBeNull();
|
|
407
|
+
},
|
|
408
|
+
);
|
|
409
|
+
it("uses supplied controls and keeps the path stable on hover", () => {
|
|
410
|
+
const view = render(
|
|
411
|
+
<svg>
|
|
412
|
+
<ColoredEdge
|
|
413
|
+
{...props}
|
|
414
|
+
data={{ ...data, dependencyLayout: "expanded" }}
|
|
415
|
+
/>
|
|
416
|
+
</svg>,
|
|
417
|
+
);
|
|
418
|
+
const d = view.container.querySelector("path#internal").getAttribute("d");
|
|
419
|
+
fireEvent.mouseEnter(
|
|
420
|
+
view.container.querySelector(".td-graph-edge-hit-area"),
|
|
421
|
+
);
|
|
422
|
+
expect(
|
|
423
|
+
view.container.querySelector("path#internal").getAttribute("d"),
|
|
424
|
+
).toBe(d);
|
|
425
|
+
view.rerender(
|
|
426
|
+
<svg>
|
|
427
|
+
<ColoredEdge
|
|
428
|
+
{...props}
|
|
429
|
+
data={{
|
|
430
|
+
...data,
|
|
431
|
+
dependencyLayout: "expanded",
|
|
432
|
+
routeGuide: { ...data.routeGuide, laneX: 460 },
|
|
433
|
+
}}
|
|
434
|
+
/>
|
|
435
|
+
</svg>,
|
|
436
|
+
);
|
|
437
|
+
expect(
|
|
438
|
+
view.container.querySelector("path#internal").getAttribute("d"),
|
|
439
|
+
).not.toBe(d);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe("isCompactInternalEdgeVisible", () => {
|
|
444
|
+
const edge = {
|
|
445
|
+
id: "internal",
|
|
446
|
+
source: "source",
|
|
447
|
+
target: "target",
|
|
448
|
+
data: { internalOwnerId: "group" },
|
|
449
|
+
};
|
|
450
|
+
const visibility = (context = {}) =>
|
|
451
|
+
isCompactInternalEdgeVisible({
|
|
452
|
+
edge,
|
|
453
|
+
dependencyLayout: "compact",
|
|
454
|
+
...context,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it.each([
|
|
458
|
+
["selected edge", { selectedEdgeId: "internal" }],
|
|
459
|
+
["keyboard focus", { keyboardFocusedNodeId: "source" }],
|
|
460
|
+
["node hover", { hoveredNodeId: "target" }],
|
|
461
|
+
])("reveals an internal dependency for %s", (_label, context) => {
|
|
462
|
+
expect(visibility(context)).toBe(true);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("reveals focused dependencies for the pinned or selected reference", () => {
|
|
466
|
+
expect(
|
|
467
|
+
visibility({
|
|
468
|
+
edge: { ...edge, data: { ...edge.data, focusFocused: true } },
|
|
469
|
+
referenceNodeId: "upstream-pin",
|
|
470
|
+
}),
|
|
471
|
+
).toBe(true);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("reuses guided geometry during hover and recomputes it when coordinates change", () => {
|
|
476
|
+
const laneX = jest.fn(() => 200);
|
|
477
|
+
const guide = {
|
|
478
|
+
kind: "corridor",
|
|
479
|
+
sourceStubX: 24,
|
|
480
|
+
targetStubX: 376,
|
|
481
|
+
get laneX() {
|
|
482
|
+
return laneX();
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
const data = { edgePath: "guided-bezier", routeGuide: guide };
|
|
486
|
+
const edge = (hoveredNodeId, targetX = 400) => (
|
|
487
|
+
<EdgeHoverContext.Provider value={{ hoveredNodeId }}>
|
|
488
|
+
<svg>
|
|
489
|
+
<ColoredEdge
|
|
490
|
+
id="memo-edge"
|
|
491
|
+
source="a"
|
|
492
|
+
target="b"
|
|
493
|
+
sourceX={0}
|
|
494
|
+
sourceY={0}
|
|
495
|
+
targetX={targetX}
|
|
496
|
+
targetY={100}
|
|
497
|
+
data={data}
|
|
498
|
+
/>
|
|
499
|
+
</svg>
|
|
500
|
+
</EdgeHoverContext.Provider>
|
|
501
|
+
);
|
|
502
|
+
const view = render(edge(null));
|
|
503
|
+
const calls = laneX.mock.calls.length;
|
|
504
|
+
expect(calls).toBeGreaterThan(0);
|
|
505
|
+
view.rerender(edge("a"));
|
|
506
|
+
expect(laneX).toHaveBeenCalledTimes(calls);
|
|
507
|
+
view.rerender(edge("b", 500));
|
|
508
|
+
expect(laneX.mock.calls.length).toBeGreaterThan(calls);
|
|
509
|
+
});
|
|
@@ -118,6 +118,65 @@ describe("<LineageGroupNode />", () => {
|
|
|
118
118
|
expect(rendered.getByText("1 nodo")).toBeInTheDocument();
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
it("renders compact dependency guidance and role columns while expanded", () => {
|
|
122
|
+
const rendered = render(
|
|
123
|
+
<LineageGroupNode
|
|
124
|
+
id="dependency-group"
|
|
125
|
+
data={{
|
|
126
|
+
collapsed: false,
|
|
127
|
+
internalHint: "Select a node to reveal dependencies",
|
|
128
|
+
internalSummary: "3 internal dependencies",
|
|
129
|
+
internalColumns: [
|
|
130
|
+
{
|
|
131
|
+
key: "source",
|
|
132
|
+
label: "Sources",
|
|
133
|
+
countLabel: "2 nodes",
|
|
134
|
+
title: "Source role",
|
|
135
|
+
x: 24,
|
|
136
|
+
y: 64,
|
|
137
|
+
width: 120,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
label: "Dependency group",
|
|
141
|
+
variant: "absolute",
|
|
142
|
+
}}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(
|
|
147
|
+
rendered
|
|
148
|
+
.getByText(/3 internal dependencies/)
|
|
149
|
+
.closest(".td-lineage-group-node__internal-summary"),
|
|
150
|
+
).toHaveAttribute(
|
|
151
|
+
"title",
|
|
152
|
+
"3 internal dependencies · Select a node to reveal dependencies",
|
|
153
|
+
);
|
|
154
|
+
expect(rendered.getByText("Sources · 2 nodes")).toHaveAttribute(
|
|
155
|
+
"title",
|
|
156
|
+
"Source role",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("hides compact dependency guidance and role columns while collapsed", () => {
|
|
161
|
+
const rendered = render(
|
|
162
|
+
<LineageGroupNode
|
|
163
|
+
id="collapsed-dependency-group"
|
|
164
|
+
data={{
|
|
165
|
+
collapsed: true,
|
|
166
|
+
internalSummary: "3 internal dependencies",
|
|
167
|
+
internalColumns: [
|
|
168
|
+
{ key: "source", label: "Sources", countLabel: "2 nodes" },
|
|
169
|
+
],
|
|
170
|
+
label: "Dependency group",
|
|
171
|
+
variant: "absolute",
|
|
172
|
+
}}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(rendered.queryByText(/3 internal dependencies/)).toBeNull();
|
|
177
|
+
expect(rendered.queryByText("Sources · 2 nodes")).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
121
180
|
it("retains an explicit classic modifier", () => {
|
|
122
181
|
const rendered = render(
|
|
123
182
|
<LineageGroupNode
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { screen } from "@testing-library/react";
|
|
1
|
+
import { act, screen, waitFor } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
import { render } from "@truedat/test/render";
|
|
4
4
|
import { TrueModal } from "../index";
|
|
@@ -190,4 +190,27 @@ describe("<TrueModal />", () => {
|
|
|
190
190
|
rerender(<TrueModal open={false} title="Title" />);
|
|
191
191
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
192
192
|
});
|
|
193
|
+
it("focuses the dialog and wraps keyboard focus within its controls", async () => {
|
|
194
|
+
const rects = jest
|
|
195
|
+
.spyOn(HTMLElement.prototype, "getClientRects")
|
|
196
|
+
.mockReturnValue([{}]);
|
|
197
|
+
renderModal({
|
|
198
|
+
children: <input aria-label="Name" />,
|
|
199
|
+
actions: [{ key: "save", content: "Save" }],
|
|
200
|
+
});
|
|
201
|
+
act(() => jest.advanceTimersByTime(20));
|
|
202
|
+
const close = screen.getByRole("button", { name: "Close" });
|
|
203
|
+
await waitFor(() => expect(close).toHaveFocus());
|
|
204
|
+
const user = setupUser();
|
|
205
|
+
await user.tab({ shift: true });
|
|
206
|
+
expect(screen.getByRole("button", { name: "Save" })).toHaveFocus();
|
|
207
|
+
await user.tab();
|
|
208
|
+
expect(close).toHaveFocus();
|
|
209
|
+
rects.mockRestore();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("exposes disabled semantics on the close control while an operation is pending", () => {
|
|
213
|
+
renderModal({ closeDisabled: true });
|
|
214
|
+
expect(screen.getByRole("button", { name: "Close" })).toBeDisabled();
|
|
215
|
+
});
|
|
193
216
|
});
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
memo,
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
|
+
useMemo,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
8
9
|
} from "react";
|
|
@@ -376,14 +377,89 @@ export const getFlexibleEdgePath = ({
|
|
|
376
377
|
};
|
|
377
378
|
};
|
|
378
379
|
|
|
380
|
+
export const getGuidedBezierEdgePath = ({
|
|
381
|
+
sourceX,
|
|
382
|
+
sourceY,
|
|
383
|
+
targetX,
|
|
384
|
+
targetY,
|
|
385
|
+
sourcePosition = "right",
|
|
386
|
+
targetPosition = "left",
|
|
387
|
+
routeGuide,
|
|
388
|
+
}) => {
|
|
389
|
+
if (!routeGuide)
|
|
390
|
+
return getFlexibleEdgePath({
|
|
391
|
+
sourceX,
|
|
392
|
+
sourceY,
|
|
393
|
+
targetX,
|
|
394
|
+
targetY,
|
|
395
|
+
sourcePosition,
|
|
396
|
+
targetPosition,
|
|
397
|
+
});
|
|
398
|
+
const sourceVector = portVector(sourcePosition),
|
|
399
|
+
targetVector = portVector(targetPosition);
|
|
400
|
+
const sourceStubX = routeGuide.sourceStubX ?? sourceX + sourceVector.x * 24;
|
|
401
|
+
const targetStubX = routeGuide.targetStubX ?? targetX + targetVector.x * 24;
|
|
402
|
+
const corridor = routeGuide.kind === "corridor";
|
|
403
|
+
const points = corridor
|
|
404
|
+
? []
|
|
405
|
+
: dedupePoints([
|
|
406
|
+
point(sourceX, sourceY),
|
|
407
|
+
point(sourceStubX, sourceY),
|
|
408
|
+
point(routeGuide.sourceLaneX, sourceY),
|
|
409
|
+
point(routeGuide.sourceLaneX, routeGuide.laneY),
|
|
410
|
+
point(routeGuide.targetLaneX, routeGuide.laneY),
|
|
411
|
+
point(routeGuide.targetLaneX, targetY),
|
|
412
|
+
point(targetStubX, targetY),
|
|
413
|
+
point(targetX, targetY),
|
|
414
|
+
]);
|
|
415
|
+
const path = corridor
|
|
416
|
+
? `M ${sourceX},${sourceY} L ${sourceStubX},${sourceY} C ${routeGuide.laneX},${sourceY} ${routeGuide.laneX},${targetY} ${targetStubX},${targetY} L ${targetX},${targetY}`
|
|
417
|
+
: roundedOrthogonalPath(points, routeGuide.cornerRadius ?? 24);
|
|
418
|
+
return {
|
|
419
|
+
path,
|
|
420
|
+
labelX: corridor
|
|
421
|
+
? getBezierPoint(
|
|
422
|
+
0.5,
|
|
423
|
+
sourceStubX,
|
|
424
|
+
routeGuide.laneX,
|
|
425
|
+
routeGuide.laneX,
|
|
426
|
+
targetStubX,
|
|
427
|
+
)
|
|
428
|
+
: routeGuide.labelPoint.x,
|
|
429
|
+
labelY: corridor ? (sourceY + targetY) / 2 : routeGuide.labelPoint.y,
|
|
430
|
+
sourceTangent: sourceVector,
|
|
431
|
+
targetTangent: normalizeVector(-targetVector.x, -targetVector.y),
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
export const isCompactInternalEdgeVisible = ({
|
|
436
|
+
edge,
|
|
437
|
+
dependencyLayout,
|
|
438
|
+
referenceNodeId,
|
|
439
|
+
selectedEdgeId,
|
|
440
|
+
keyboardFocusedNodeId,
|
|
441
|
+
hoveredNodeId,
|
|
442
|
+
}) => {
|
|
443
|
+
if (dependencyLayout !== "compact" || !edge.data?.internalOwnerId)
|
|
444
|
+
return true;
|
|
445
|
+
if (edge.id === selectedEdgeId) return true;
|
|
446
|
+
if (referenceNodeId && edge.data?.focusFocused) return true;
|
|
447
|
+
return [referenceNodeId, keyboardFocusedNodeId, hoveredNodeId].some(
|
|
448
|
+
(activeNodeId) =>
|
|
449
|
+
activeNodeId &&
|
|
450
|
+
(edge.source === activeNodeId || edge.target === activeNodeId),
|
|
451
|
+
);
|
|
452
|
+
};
|
|
453
|
+
|
|
379
454
|
export const getEdgeSlotOffsets = (data = {}) => ({
|
|
380
455
|
sourceSlotOffset: data.lockSourcePort
|
|
381
456
|
? 0
|
|
382
457
|
: data.sourceFieldIndex === undefined
|
|
383
458
|
? getTargetSlotOffset(data.sourceSlotIndex, data.sourceSlotCount)
|
|
384
459
|
: 0,
|
|
385
|
-
targetSlotOffset:
|
|
386
|
-
|
|
460
|
+
targetSlotOffset: data.lockTargetPort
|
|
461
|
+
? 0
|
|
462
|
+
: data.targetFieldIndex === undefined
|
|
387
463
|
? getTargetSlotOffset(
|
|
388
464
|
data.targetSlotIndex,
|
|
389
465
|
data.targetSlotCount,
|
|
@@ -394,7 +470,11 @@ export const getEdgeSlotOffsets = (data = {}) => ({
|
|
|
394
470
|
: 0,
|
|
395
471
|
});
|
|
396
472
|
|
|
397
|
-
export const getLineageEdgeVisualState = (
|
|
473
|
+
export const getLineageEdgeVisualState = (
|
|
474
|
+
data = {},
|
|
475
|
+
hovered = false,
|
|
476
|
+
visible = true,
|
|
477
|
+
) => {
|
|
398
478
|
const aggregatedCount = Number(data.aggregatedCount);
|
|
399
479
|
const aggregated = Number.isFinite(aggregatedCount) && aggregatedCount > 1;
|
|
400
480
|
const dimmed = Boolean(data.focusDimmed) && !hovered;
|
|
@@ -403,9 +483,10 @@ export const getLineageEdgeVisualState = (data = {}, hovered = false) => {
|
|
|
403
483
|
aggregated,
|
|
404
484
|
highlighted:
|
|
405
485
|
!data.containment && !dimmed && (hovered || Boolean(data.focusFocused)),
|
|
406
|
-
showDecorations: !dimmed,
|
|
407
|
-
showHitArea:
|
|
486
|
+
showDecorations: visible && !dimmed,
|
|
487
|
+
showHitArea: visible,
|
|
408
488
|
showLabel:
|
|
489
|
+
visible &&
|
|
409
490
|
!dimmed &&
|
|
410
491
|
(aggregated
|
|
411
492
|
? Boolean(data.selectedEndpoint)
|
|
@@ -430,6 +511,14 @@ const ColoredEdge = ({
|
|
|
430
511
|
const hoverCtx = useContext(EdgeHoverContext);
|
|
431
512
|
const hoveredNodeId = hoverCtx?.hoveredNodeId;
|
|
432
513
|
const hoveredEdgeEndpoints = hoverCtx?.hoveredEdgeEndpoints;
|
|
514
|
+
const compactVisible = isCompactInternalEdgeVisible({
|
|
515
|
+
edge: { id, source, target, data },
|
|
516
|
+
dependencyLayout: data.dependencyLayout,
|
|
517
|
+
referenceNodeId: hoverCtx?.compactReferenceNodeId,
|
|
518
|
+
selectedEdgeId: hoverCtx?.selectedEdgeId,
|
|
519
|
+
keyboardFocusedNodeId: hoverCtx?.keyboardFocusedNodeId,
|
|
520
|
+
hoveredNodeId,
|
|
521
|
+
});
|
|
433
522
|
const [hovered, setHovered] = useState(false);
|
|
434
523
|
const [motionTiming, setMotionTiming] = useState(null);
|
|
435
524
|
const gRef = useRef(null);
|
|
@@ -440,12 +529,14 @@ const ColoredEdge = ({
|
|
|
440
529
|
const interactive = Boolean(data.onClick);
|
|
441
530
|
const { sourceSlotOffset, targetSlotOffset } = getEdgeSlotOffsets(data);
|
|
442
531
|
const useOrthogonalPath = data.edgePath === "orthogonal";
|
|
532
|
+
const useGuidedBezierPath = data.edgePath === "guided-bezier";
|
|
533
|
+
const useGuidedPath = useOrthogonalPath || useGuidedBezierPath;
|
|
443
534
|
const useFlexibleBezierPath = data.edgePath === "bezier";
|
|
444
|
-
const useClassicEdgePath = !
|
|
535
|
+
const useClassicEdgePath = !useGuidedPath && !useFlexibleBezierPath;
|
|
445
536
|
const adjustedSourceY = sourceY + (data.bundleSource ? 0 : sourceSlotOffset);
|
|
446
537
|
const adjustedTargetY = targetY + (data.bundleTarget ? 0 : targetSlotOffset);
|
|
447
538
|
const { aggregated, highlighted, showDecorations, showHitArea, showLabel } =
|
|
448
|
-
getLineageEdgeVisualState(data, hovered);
|
|
539
|
+
getLineageEdgeVisualState(data, hovered, compactVisible);
|
|
449
540
|
const dependenciesOnLeft =
|
|
450
541
|
data.selectedEndpoint === "source" ? targetX < sourceX : sourceX < targetX;
|
|
451
542
|
const setHoveredEdgeEndpoints = hoverCtx?.setHoveredEdgeEndpoints;
|
|
@@ -458,13 +549,13 @@ const ColoredEdge = ({
|
|
|
458
549
|
};
|
|
459
550
|
}, [id, showHitArea, setHoveredEdgeEndpoints]);
|
|
460
551
|
const forceHorizontalPorts = Boolean(
|
|
461
|
-
|
|
552
|
+
useGuidedPath && data.forceHorizontalPorts,
|
|
462
553
|
);
|
|
463
554
|
const stroke = style.stroke || "var(--td-graph-edge-stroke, #b0b8c8)";
|
|
464
555
|
const markerPosition = data.markerPosition;
|
|
465
556
|
const showTargetMarker =
|
|
466
557
|
showDecorations &&
|
|
467
|
-
(
|
|
558
|
+
(useGuidedPath
|
|
468
559
|
? markerPosition === "target" ||
|
|
469
560
|
markerPosition === "both" ||
|
|
470
561
|
(markerPosition === undefined && data.showTargetArrow !== false)
|
|
@@ -483,7 +574,19 @@ const ColoredEdge = ({
|
|
|
483
574
|
const sourcePortY = adjustedSourceY;
|
|
484
575
|
const targetPortX = targetX;
|
|
485
576
|
const targetPortY = adjustedTargetY;
|
|
486
|
-
const
|
|
577
|
+
const geometry = useMemo(() => {
|
|
578
|
+
if (useGuidedBezierPath) {
|
|
579
|
+
const curve = getGuidedBezierEdgePath({
|
|
580
|
+
sourceX: sourcePortX,
|
|
581
|
+
sourceY: sourcePortY,
|
|
582
|
+
targetX: targetPortX,
|
|
583
|
+
targetY: targetPortY,
|
|
584
|
+
sourcePosition: effectiveSourcePosition,
|
|
585
|
+
targetPosition: effectiveTargetPosition,
|
|
586
|
+
routeGuide: data.routeGuide,
|
|
587
|
+
});
|
|
588
|
+
return { ...curve, edgePath: curve.path };
|
|
589
|
+
}
|
|
487
590
|
if (useOrthogonalPath) {
|
|
488
591
|
const orthogonalEdge = getOrthogonalEdgePath({
|
|
489
592
|
sourceX: sourcePortX,
|
|
@@ -606,9 +709,37 @@ const ColoredEdge = ({
|
|
|
606
709
|
|
|
607
710
|
return { edgePath, labelX, labelY, targetTangent, sourceTangent };
|
|
608
711
|
}
|
|
609
|
-
}
|
|
712
|
+
}, [
|
|
713
|
+
useGuidedBezierPath,
|
|
714
|
+
useOrthogonalPath,
|
|
715
|
+
useFlexibleBezierPath,
|
|
716
|
+
sourcePortX,
|
|
717
|
+
sourcePortY,
|
|
718
|
+
targetPortX,
|
|
719
|
+
targetPortY,
|
|
720
|
+
effectiveSourcePosition,
|
|
721
|
+
effectiveTargetPosition,
|
|
722
|
+
sourceX,
|
|
723
|
+
targetX,
|
|
724
|
+
adjustedSourceY,
|
|
725
|
+
adjustedTargetY,
|
|
726
|
+
sourcePosition,
|
|
727
|
+
targetPosition,
|
|
728
|
+
data.routeGuide,
|
|
729
|
+
data.edgeBorderRadius,
|
|
730
|
+
data.edgeOffset,
|
|
731
|
+
data.routeIndex,
|
|
732
|
+
data.routeCount,
|
|
733
|
+
data.sourceNodeY,
|
|
734
|
+
data.targetNodeY,
|
|
735
|
+
data.edgeCurvature,
|
|
736
|
+
data.bundleSource,
|
|
737
|
+
data.bundleTarget,
|
|
738
|
+
data.bundleOffset,
|
|
739
|
+
]);
|
|
740
|
+
const { edgePath, labelX, labelY, targetTangent, sourceTangent } = geometry;
|
|
610
741
|
useEffect(() => {
|
|
611
|
-
if (!data.animated || !gRef.current) return;
|
|
742
|
+
if (!compactVisible || !data.animated || !gRef.current) return;
|
|
612
743
|
|
|
613
744
|
const pathElement = gRef.current.querySelector("path.td-graph-edge");
|
|
614
745
|
|
|
@@ -623,7 +754,7 @@ const ColoredEdge = ({
|
|
|
623
754
|
? current
|
|
624
755
|
: { duration, path: edgePath },
|
|
625
756
|
);
|
|
626
|
-
}, [data.animated, edgePath]);
|
|
757
|
+
}, [compactVisible, data.animated, edgePath]);
|
|
627
758
|
const animationDuration =
|
|
628
759
|
motionTiming?.path === edgePath ? motionTiming.duration : null;
|
|
629
760
|
const nodeHovered = hoveredNodeId
|
|
@@ -643,7 +774,7 @@ const ColoredEdge = ({
|
|
|
643
774
|
? "var(--td-graph-edge-active, var(--td-graph-accent, #ed5c17))"
|
|
644
775
|
: stroke;
|
|
645
776
|
const handleClick = (event) => {
|
|
646
|
-
if (!interactive) return;
|
|
777
|
+
if (!interactive || !compactVisible) return;
|
|
647
778
|
event.stopPropagation();
|
|
648
779
|
data.onClick(data.metadata);
|
|
649
780
|
};
|
|
@@ -664,7 +795,13 @@ const ColoredEdge = ({
|
|
|
664
795
|
.join(" ");
|
|
665
796
|
|
|
666
797
|
return (
|
|
667
|
-
<g
|
|
798
|
+
<g
|
|
799
|
+
ref={gRef}
|
|
800
|
+
style={
|
|
801
|
+
compactVisible ? undefined : { display: "none", pointerEvents: "none" }
|
|
802
|
+
}
|
|
803
|
+
aria-hidden={!compactVisible || undefined}
|
|
804
|
+
>
|
|
668
805
|
<BaseEdge
|
|
669
806
|
className={edgeClassName}
|
|
670
807
|
id={id}
|
|
@@ -674,7 +811,7 @@ const ColoredEdge = ({
|
|
|
674
811
|
stroke: effectiveStroke,
|
|
675
812
|
}}
|
|
676
813
|
/>
|
|
677
|
-
{data.animated && animationDuration ? (
|
|
814
|
+
{compactVisible && data.animated && animationDuration ? (
|
|
678
815
|
<circle
|
|
679
816
|
aria-hidden="true"
|
|
680
817
|
className="td-graph-edge-flow-dot"
|
|
@@ -49,6 +49,9 @@ const LineageGroupNode = ({ id, data = {} }) => {
|
|
|
49
49
|
oversized,
|
|
50
50
|
oversizedWarningOpen,
|
|
51
51
|
resourceContainer,
|
|
52
|
+
internalColumns = [],
|
|
53
|
+
internalSummary,
|
|
54
|
+
internalHint,
|
|
52
55
|
systemTypeClassName,
|
|
53
56
|
systemTypeLabel,
|
|
54
57
|
systemTypeMissing,
|
|
@@ -195,6 +198,30 @@ const LineageGroupNode = ({ id, data = {} }) => {
|
|
|
195
198
|
) : null}
|
|
196
199
|
</div>
|
|
197
200
|
) : null}
|
|
201
|
+
{!collapsed && internalSummary ? (
|
|
202
|
+
<div
|
|
203
|
+
className="td-lineage-group-node__internal-summary"
|
|
204
|
+
title={[internalSummary, internalHint].filter(Boolean).join(" · ")}
|
|
205
|
+
>
|
|
206
|
+
<Icon aria-hidden="true" name="info circle" />
|
|
207
|
+
<span>
|
|
208
|
+
{internalSummary}
|
|
209
|
+
{internalHint ? ` · ${internalHint}` : null}
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
) : null}
|
|
213
|
+
{!collapsed
|
|
214
|
+
? internalColumns.map((column) => (
|
|
215
|
+
<span
|
|
216
|
+
key={`${column.key}-${column.y}`}
|
|
217
|
+
className="td-lineage-group-node__column-label"
|
|
218
|
+
title={column.title}
|
|
219
|
+
style={{ left: column.x, top: column.y, width: column.width }}
|
|
220
|
+
>
|
|
221
|
+
{column.label} · {column.countLabel ?? column.count}
|
|
222
|
+
</span>
|
|
223
|
+
))
|
|
224
|
+
: null}
|
|
198
225
|
{showContentSummary ? (
|
|
199
226
|
<div className="td-lineage-group-node__footer">
|
|
200
227
|
<span className="td-lineage-group-node__content-summary">
|
|
@@ -296,6 +323,20 @@ LineageGroupNode.propTypes = {
|
|
|
296
323
|
oversized: PropTypes.bool,
|
|
297
324
|
oversizedWarningOpen: PropTypes.bool,
|
|
298
325
|
resourceContainer: PropTypes.bool,
|
|
326
|
+
internalColumns: PropTypes.arrayOf(
|
|
327
|
+
PropTypes.shape({
|
|
328
|
+
key: PropTypes.string,
|
|
329
|
+
label: PropTypes.string,
|
|
330
|
+
title: PropTypes.string,
|
|
331
|
+
x: PropTypes.number,
|
|
332
|
+
y: PropTypes.number,
|
|
333
|
+
width: PropTypes.number,
|
|
334
|
+
count: PropTypes.number,
|
|
335
|
+
countLabel: PropTypes.string,
|
|
336
|
+
}),
|
|
337
|
+
),
|
|
338
|
+
internalSummary: PropTypes.string,
|
|
339
|
+
internalHint: PropTypes.string,
|
|
299
340
|
systemTypeClassName: PropTypes.string,
|
|
300
341
|
systemTypeLabel: PropTypes.string,
|
|
301
342
|
systemTypeMissing: PropTypes.bool,
|
|
@@ -556,3 +556,42 @@
|
|
|
556
556
|
.td-lineage-group-node.td-lineage-group-node {
|
|
557
557
|
.lineage-node-states(td-lineage-group-node);
|
|
558
558
|
}
|
|
559
|
+
|
|
560
|
+
.td-lineage-group-node__column-label {
|
|
561
|
+
position: absolute;
|
|
562
|
+
height: 24px;
|
|
563
|
+
color: #64748b;
|
|
564
|
+
font-size: 12px;
|
|
565
|
+
font-weight: 700;
|
|
566
|
+
line-height: 24px;
|
|
567
|
+
pointer-events: none;
|
|
568
|
+
}
|
|
569
|
+
.td-lineage-group-node__internal-summary {
|
|
570
|
+
position: absolute;
|
|
571
|
+
top: 42px;
|
|
572
|
+
right: 0;
|
|
573
|
+
left: 0;
|
|
574
|
+
display: flex;
|
|
575
|
+
height: 16px;
|
|
576
|
+
align-items: center;
|
|
577
|
+
gap: 4px;
|
|
578
|
+
padding: 0 6px;
|
|
579
|
+
background: #eff6ff;
|
|
580
|
+
color: #1e40af;
|
|
581
|
+
font-size: 10px;
|
|
582
|
+
font-weight: 400;
|
|
583
|
+
line-height: 16px;
|
|
584
|
+
|
|
585
|
+
> .icon {
|
|
586
|
+
flex-shrink: 0;
|
|
587
|
+
height: 16px;
|
|
588
|
+
margin: 0;
|
|
589
|
+
line-height: 16px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
> span {
|
|
593
|
+
overflow: hidden;
|
|
594
|
+
text-overflow: ellipsis;
|
|
595
|
+
white-space: nowrap;
|
|
596
|
+
}
|
|
597
|
+
}
|