@truedat/dd 8.14.0 → 8.14.1
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 +177 -30
- package/src/components/LineageGraphToolbar.js +174 -25
- package/src/components/__tests__/LineageGraph.spec.js +98 -0
- package/src/components/__tests__/LineageGraphInteraction.spec.js +278 -13
- package/src/components/__tests__/LineageGraphToolbar.spec.js +86 -0
- package/src/styles/LineageGraph.less +35 -0
- package/src/utils/__tests__/fixtures/lineageInternalDependencies.json +375 -0
- package/src/utils/__tests__/lineageInternalRoutes.spec.js +202 -0
- package/src/utils/__tests__/lineageLayout.spec.js +6 -6
- package/src/utils/__tests__/lineageViewPreferences.spec.js +27 -0
- package/src/utils/lineageGraph.js +56 -11
- package/src/utils/lineageInternalRoutes.js +389 -0
- package/src/utils/lineageLayout.js +32 -12
- package/src/utils/lineageViewPreferences.js +21 -0
|
@@ -1002,6 +1002,21 @@ export const lineageNodeContentWidth = (resources = []) => {
|
|
|
1002
1002
|
);
|
|
1003
1003
|
};
|
|
1004
1004
|
|
|
1005
|
+
const lineageSiblingWidths = (nodes, parentFor) => {
|
|
1006
|
+
const widths = new Map();
|
|
1007
|
+
nodes.forEach((node) => {
|
|
1008
|
+
const parent = parentFor(node);
|
|
1009
|
+
widths.set(
|
|
1010
|
+
parent,
|
|
1011
|
+
Math.max(widths.get(parent) || 0, lineageNodeContentWidth([node])),
|
|
1012
|
+
);
|
|
1013
|
+
});
|
|
1014
|
+
return (node) => widths.get(parentFor(node)) || LINEAGE_NODE_WIDTH_MIN;
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
const resolvedLineageNodeWidth = (nodeWidth, node) =>
|
|
1018
|
+
typeof nodeWidth === "function" ? nodeWidth(node) : nodeWidth;
|
|
1019
|
+
|
|
1005
1020
|
const lineagePath = (node, system, schema) => {
|
|
1006
1021
|
const path = node.path || node.ancestry || node.parents || node.parent_path;
|
|
1007
1022
|
const pathItems = Array.isArray(path)
|
|
@@ -1090,7 +1105,9 @@ const lineageNode =
|
|
|
1090
1105
|
const systemType = lineageSystemType(node);
|
|
1091
1106
|
const entity = lineageEntityPresentation(node, resolvedKind);
|
|
1092
1107
|
const path = redesigned ? lineagePath(node, system, schema) : [];
|
|
1093
|
-
const width = redesigned
|
|
1108
|
+
const width = redesigned
|
|
1109
|
+
? resolvedLineageNodeWidth(nodeWidth, node) || NODE_WIDTH_REDESIGNED
|
|
1110
|
+
: NODE_WIDTH;
|
|
1094
1111
|
|
|
1095
1112
|
return {
|
|
1096
1113
|
id,
|
|
@@ -1334,8 +1351,16 @@ const lineagePositionedResourceNode =
|
|
|
1334
1351
|
export const lineageTopologyNodes = (
|
|
1335
1352
|
topology,
|
|
1336
1353
|
nodeDesign = "redesigned",
|
|
1337
|
-
nodeWidth
|
|
1354
|
+
nodeWidth,
|
|
1338
1355
|
) => {
|
|
1356
|
+
const widthFor =
|
|
1357
|
+
nodeWidth ??
|
|
1358
|
+
(nodeDesign === "redesigned"
|
|
1359
|
+
? lineageSiblingWidths(
|
|
1360
|
+
topology.nodes.filter((node) => !node.container),
|
|
1361
|
+
(node) => node.parentId,
|
|
1362
|
+
)
|
|
1363
|
+
: NODE_WIDTH_REDESIGNED);
|
|
1339
1364
|
const parentByNode = new Map();
|
|
1340
1365
|
const childCountsByCanonicalId = new Map();
|
|
1341
1366
|
const nodeById = new Map(topology.nodes.map((node) => [node.id, node]));
|
|
@@ -1345,7 +1370,7 @@ export const lineageTopologyNodes = (
|
|
|
1345
1370
|
geometryById.set(node.id, {
|
|
1346
1371
|
x: 0,
|
|
1347
1372
|
y: 0,
|
|
1348
|
-
w:
|
|
1373
|
+
w: resolvedLineageNodeWidth(widthFor, node),
|
|
1349
1374
|
h: COLLAPSED_GROUP_HEIGHT,
|
|
1350
1375
|
});
|
|
1351
1376
|
if (!node.parentId) return;
|
|
@@ -1376,7 +1401,7 @@ export const lineageTopologyNodes = (
|
|
|
1376
1401
|
resolveSystemType,
|
|
1377
1402
|
new Set(),
|
|
1378
1403
|
undefined,
|
|
1379
|
-
|
|
1404
|
+
widthFor,
|
|
1380
1405
|
);
|
|
1381
1406
|
return [...topology.nodes]
|
|
1382
1407
|
.sort((a, b) => a.z - b.z || a.id.localeCompare(b.id))
|
|
@@ -2487,13 +2512,18 @@ export const lineagePositionedLayoutNodes = ({
|
|
|
2487
2512
|
preparedHierarchy = preparePositionedLineageHierarchy(groups, resources),
|
|
2488
2513
|
}) => {
|
|
2489
2514
|
const { containerGroups, leafGroups, parentByNode } = preparedHierarchy;
|
|
2515
|
+
const widthFor =
|
|
2516
|
+
nodeWidth ??
|
|
2517
|
+
lineageSiblingWidths([...resources, ...leafGroups], (node) =>
|
|
2518
|
+
parentByNode.get(node),
|
|
2519
|
+
);
|
|
2490
2520
|
const leafGroupIds = new Set(leafGroups.map(nodeId));
|
|
2491
2521
|
const positionedResourceGeometry = (node) =>
|
|
2492
2522
|
positionedNodeGeometryForDesign(
|
|
2493
2523
|
node,
|
|
2494
2524
|
nodeDesign,
|
|
2495
2525
|
leafGroupIds.has(nodeId(node)) ? "group" : "resource",
|
|
2496
|
-
|
|
2526
|
+
widthFor,
|
|
2497
2527
|
);
|
|
2498
2528
|
const geometryById = buildPositionedGeometryMap(
|
|
2499
2529
|
containerGroups,
|
|
@@ -2529,7 +2559,7 @@ export const lineagePositionedLayoutNodes = ({
|
|
|
2529
2559
|
resolveSystemType,
|
|
2530
2560
|
expandedFieldNodeIds,
|
|
2531
2561
|
onToggleFields,
|
|
2532
|
-
|
|
2562
|
+
widthFor,
|
|
2533
2563
|
),
|
|
2534
2564
|
),
|
|
2535
2565
|
...resources.map(
|
|
@@ -2543,7 +2573,7 @@ export const lineagePositionedLayoutNodes = ({
|
|
|
2543
2573
|
resolveSystemType,
|
|
2544
2574
|
expandedFieldNodeIds,
|
|
2545
2575
|
onToggleFields,
|
|
2546
|
-
|
|
2576
|
+
widthFor,
|
|
2547
2577
|
),
|
|
2548
2578
|
),
|
|
2549
2579
|
]),
|
|
@@ -2717,7 +2747,7 @@ export const bundleLineageEdges = (edges = []) => {
|
|
|
2717
2747
|
const endpointKey = (nodeId, handleId) => `${nodeId}::${handleId || ""}`;
|
|
2718
2748
|
|
|
2719
2749
|
edges.forEach((edge) => {
|
|
2720
|
-
if (edge.data?.containment) return;
|
|
2750
|
+
if (edge.data?.containment || edge.data?.internalOwnerId) return;
|
|
2721
2751
|
|
|
2722
2752
|
const sourceKey = endpointKey(edge.source, edge.sourceHandle);
|
|
2723
2753
|
const targetKey = endpointKey(edge.target, edge.targetHandle);
|
|
@@ -2727,7 +2757,7 @@ export const bundleLineageEdges = (edges = []) => {
|
|
|
2727
2757
|
});
|
|
2728
2758
|
|
|
2729
2759
|
return edges.map((edge) => {
|
|
2730
|
-
if (edge.data?.containment) return edge;
|
|
2760
|
+
if (edge.data?.containment || edge.data?.internalOwnerId) return edge;
|
|
2731
2761
|
|
|
2732
2762
|
const sourceCount =
|
|
2733
2763
|
sourceCounts.get(endpointKey(edge.source, edge.sourceHandle)) || 0;
|
|
@@ -2754,8 +2784,11 @@ export const redesignedLineageEdges = (edges = []) =>
|
|
|
2754
2784
|
data: {
|
|
2755
2785
|
...edge.data,
|
|
2756
2786
|
edgeCurvature: 0.32,
|
|
2757
|
-
edgePath: "bezier",
|
|
2758
|
-
routeGuide:
|
|
2787
|
+
edgePath: edge.data?.internalOwnerId ? "guided-bezier" : "bezier",
|
|
2788
|
+
routeGuide:
|
|
2789
|
+
edge.data?.internalOwnerId || edge.data?.topology
|
|
2790
|
+
? edge.data.routeGuide
|
|
2791
|
+
: undefined,
|
|
2759
2792
|
},
|
|
2760
2793
|
})),
|
|
2761
2794
|
);
|
|
@@ -3114,6 +3147,18 @@ export const planLineageEdgeRoutes = (edges = [], nodes = []) => {
|
|
|
3114
3147
|
}));
|
|
3115
3148
|
};
|
|
3116
3149
|
|
|
3150
|
+
export const planExternalLineageEdgeRoutes = (edges = [], nodes = []) => {
|
|
3151
|
+
const plannedById = new Map(
|
|
3152
|
+
planLineageEdgeRoutes(
|
|
3153
|
+
edges.filter((edge) => !edge.data?.internalOwnerId),
|
|
3154
|
+
nodes,
|
|
3155
|
+
).map((edge) => [edge.id, edge]),
|
|
3156
|
+
);
|
|
3157
|
+
return edges.map((edge) =>
|
|
3158
|
+
edge.data?.internalOwnerId ? edge : plannedById.get(edge.id),
|
|
3159
|
+
);
|
|
3160
|
+
};
|
|
3161
|
+
|
|
3117
3162
|
export const focusedLineageGraph = (selectedNodeId, edges = []) => {
|
|
3118
3163
|
const focusedNodeIds = new Set();
|
|
3119
3164
|
const focusedEdgeIds = new Set();
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
const LINEAGE_PREVIEW_EDGE_Z_INDEX = 150;
|
|
2
|
+
|
|
3
|
+
const HEADER = 58;
|
|
4
|
+
const PADDING = 16;
|
|
5
|
+
const COLUMN_GAP = 152;
|
|
6
|
+
const PORT = 24;
|
|
7
|
+
const PITCH = 12;
|
|
8
|
+
|
|
9
|
+
const assignSlots = (items) => {
|
|
10
|
+
const ends = [];
|
|
11
|
+
const slots = new Map();
|
|
12
|
+
[...items]
|
|
13
|
+
.sort(
|
|
14
|
+
(a, b) => a.start - b.start || a.end - b.end || a.id.localeCompare(b.id),
|
|
15
|
+
)
|
|
16
|
+
.forEach(({ id, start, end }) => {
|
|
17
|
+
const available = ends.findIndex((last) => last < start);
|
|
18
|
+
const slot = available < 0 ? ends.length : available;
|
|
19
|
+
ends.splice(slot, 1, end);
|
|
20
|
+
slots.set(id, slot);
|
|
21
|
+
});
|
|
22
|
+
return { slots, count: ends.length };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const internalRoleColumns = (children, edges) => {
|
|
26
|
+
const incoming = new Set(edges.map(({ target }) => target));
|
|
27
|
+
const outgoing = new Set(edges.map(({ source }) => source));
|
|
28
|
+
const groups = { entry: [], intermediate: [], exit: [], independent: [] };
|
|
29
|
+
children.forEach((node) => {
|
|
30
|
+
const role = incoming.has(node.id)
|
|
31
|
+
? outgoing.has(node.id)
|
|
32
|
+
? "intermediate"
|
|
33
|
+
: "exit"
|
|
34
|
+
: outgoing.has(node.id)
|
|
35
|
+
? "entry"
|
|
36
|
+
: "independent";
|
|
37
|
+
groups[role].push({ ...node, role });
|
|
38
|
+
});
|
|
39
|
+
const columns = ["entry", "intermediate", "exit"]
|
|
40
|
+
.filter((key) => groups[key].length)
|
|
41
|
+
.map((key) => ({ key, nodes: groups[key] }));
|
|
42
|
+
if (groups.independent.length) {
|
|
43
|
+
if (!columns.length) columns.push({ key: "independent", nodes: [] });
|
|
44
|
+
columns[0].nodes.push(...groups.independent);
|
|
45
|
+
}
|
|
46
|
+
return columns;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const reserveInternalGroupChannels = ({
|
|
50
|
+
columns,
|
|
51
|
+
edges,
|
|
52
|
+
nodeGap = 44,
|
|
53
|
+
columnGap = COLUMN_GAP,
|
|
54
|
+
columnLabelHeight = 0,
|
|
55
|
+
compact = false,
|
|
56
|
+
}) => {
|
|
57
|
+
const lanePitch = compact ? 4 : PITCH;
|
|
58
|
+
const laneOffset = compact ? 32 : 48;
|
|
59
|
+
const lanePadding = compact ? 48 : 72;
|
|
60
|
+
const labelTop = HEADER + (columnLabelHeight ? 8 : 0);
|
|
61
|
+
const indexed = new Map();
|
|
62
|
+
const widths = columns.map(({ nodes }) =>
|
|
63
|
+
Math.max(...nodes.map(({ width }) => width)),
|
|
64
|
+
);
|
|
65
|
+
const columnEnds = columns.map(({ nodes, sectioned }, column) => {
|
|
66
|
+
const end = nodes.reduce(
|
|
67
|
+
(y, node, index) => {
|
|
68
|
+
const sectionStart =
|
|
69
|
+
index > 0 &&
|
|
70
|
+
node.role !== nodes[index - 1].role &&
|
|
71
|
+
(sectioned || node.role === "independent");
|
|
72
|
+
const top = y + (sectionStart ? 32 : 0);
|
|
73
|
+
indexed.set(node.id, { ...node, column, y: top, sectionStart });
|
|
74
|
+
return top + node.height + nodeGap;
|
|
75
|
+
},
|
|
76
|
+
labelTop + columnLabelHeight + (sectioned ? 8 : 0),
|
|
77
|
+
);
|
|
78
|
+
return end - nodeGap;
|
|
79
|
+
});
|
|
80
|
+
const bodyBottom = Math.max(...columnEnds);
|
|
81
|
+
const validEdges = edges.filter(
|
|
82
|
+
(e) =>
|
|
83
|
+
!e.data?.containment &&
|
|
84
|
+
e.source !== e.target &&
|
|
85
|
+
indexed.has(e.source) &&
|
|
86
|
+
indexed.has(e.target),
|
|
87
|
+
);
|
|
88
|
+
const routes = [...validEdges]
|
|
89
|
+
.sort(
|
|
90
|
+
(a, b) =>
|
|
91
|
+
a.source.localeCompare(b.source) ||
|
|
92
|
+
a.target.localeCompare(b.target) ||
|
|
93
|
+
String(a.id || "").localeCompare(String(b.id || "")),
|
|
94
|
+
)
|
|
95
|
+
.map((edge, index) => {
|
|
96
|
+
const source = indexed.get(edge.source),
|
|
97
|
+
target = indexed.get(edge.target);
|
|
98
|
+
const sourceY = source.y + source.height / 2,
|
|
99
|
+
targetY = target.y + target.height / 2;
|
|
100
|
+
const sameColumn = source.column === target.column;
|
|
101
|
+
const corridor = sameColumn || target.column === source.column + 1;
|
|
102
|
+
const topDistance =
|
|
103
|
+
sourceY + targetY - 2 * (labelTop + columnLabelHeight);
|
|
104
|
+
const bottomDistance = 2 * bodyBottom - sourceY - targetY;
|
|
105
|
+
return {
|
|
106
|
+
edge,
|
|
107
|
+
id: edge.id || `${edge.source}->${edge.target}:${index}`,
|
|
108
|
+
source,
|
|
109
|
+
target,
|
|
110
|
+
sourceY,
|
|
111
|
+
targetY,
|
|
112
|
+
sameColumn,
|
|
113
|
+
corridor,
|
|
114
|
+
side: topDistance <= bottomDistance ? "top" : "bottom",
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
const boundaryItems = new Map();
|
|
118
|
+
const buses = { top: [], bottom: [] };
|
|
119
|
+
const addBoundary = (boundary, id, a, b) => {
|
|
120
|
+
const items = boundaryItems.get(boundary) || [];
|
|
121
|
+
items.push({ id, start: Math.min(a, b), end: Math.max(a, b) });
|
|
122
|
+
boundaryItems.set(boundary, items);
|
|
123
|
+
};
|
|
124
|
+
routes.forEach((r) => {
|
|
125
|
+
if (r.corridor) addBoundary(r.source.column, r.id, r.sourceY, r.targetY);
|
|
126
|
+
else {
|
|
127
|
+
const limit = r.side === "top" ? -Infinity : Infinity;
|
|
128
|
+
addBoundary(r.source.column, `${r.id}:source`, r.sourceY, limit);
|
|
129
|
+
addBoundary(r.target.column - 1, `${r.id}:target`, r.targetY, limit);
|
|
130
|
+
buses[r.side].push({
|
|
131
|
+
id: r.id,
|
|
132
|
+
start: Math.min(r.source.column, r.target.column - 1),
|
|
133
|
+
end: Math.max(r.source.column, r.target.column - 1),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const boundarySlots = new Map(
|
|
138
|
+
[...boundaryItems].map(([key, items]) => [key, assignSlots(items)]),
|
|
139
|
+
);
|
|
140
|
+
const busSlots = {
|
|
141
|
+
top: assignSlots(buses.top),
|
|
142
|
+
bottom: assignSlots(buses.bottom),
|
|
143
|
+
};
|
|
144
|
+
const topReserve = busSlots.top.count ? 36 + busSlots.top.count * PITCH : 0;
|
|
145
|
+
const bottomReserve = busSlots.bottom.count
|
|
146
|
+
? 36 + busSlots.bottom.count * PITCH
|
|
147
|
+
: 12;
|
|
148
|
+
const boundaryWidth = (index) =>
|
|
149
|
+
Math.max(
|
|
150
|
+
columnGap,
|
|
151
|
+
lanePadding + (boundarySlots.get(index)?.count || 0) * lanePitch,
|
|
152
|
+
);
|
|
153
|
+
const outsideWidth = (index) =>
|
|
154
|
+
boundarySlots.has(index)
|
|
155
|
+
? lanePadding + boundarySlots.get(index).count * lanePitch
|
|
156
|
+
: PADDING;
|
|
157
|
+
const positions = [];
|
|
158
|
+
columns.forEach((_column, index) => {
|
|
159
|
+
positions.push(
|
|
160
|
+
index
|
|
161
|
+
? positions[index - 1] + widths[index - 1] + boundaryWidth(index - 1)
|
|
162
|
+
: outsideWidth(-1),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
const geometries = new Map(
|
|
166
|
+
[...indexed].map(([id, n]) => [
|
|
167
|
+
id,
|
|
168
|
+
{ x: positions[n.column], y: n.y + topReserve, w: n.width, h: n.height },
|
|
169
|
+
]),
|
|
170
|
+
);
|
|
171
|
+
const boundaryX = (index, id) => {
|
|
172
|
+
const start = index < 0 ? 0 : positions[index] + widths[index];
|
|
173
|
+
return (
|
|
174
|
+
start + laneOffset + boundarySlots.get(index).slots.get(id) * lanePitch
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
const guidesByEdgeId = new Map();
|
|
178
|
+
routes.forEach((r) => {
|
|
179
|
+
const source = geometries.get(r.edge.source),
|
|
180
|
+
target = geometries.get(r.edge.target);
|
|
181
|
+
const sourceY = source.y + source.h / 2,
|
|
182
|
+
targetY = target.y + target.h / 2;
|
|
183
|
+
const sourceStubX =
|
|
184
|
+
positions[r.source.column] + widths[r.source.column] + PORT;
|
|
185
|
+
const targetStubX = r.sameColumn
|
|
186
|
+
? positions[r.target.column] + widths[r.target.column] + PORT
|
|
187
|
+
: target.x - PORT;
|
|
188
|
+
const guide = {
|
|
189
|
+
sourceStubX,
|
|
190
|
+
targetStubX,
|
|
191
|
+
sourcePortPosition: "right",
|
|
192
|
+
targetPortPosition: r.sameColumn ? "right" : "left",
|
|
193
|
+
cornerRadius: 24,
|
|
194
|
+
};
|
|
195
|
+
if (r.corridor) {
|
|
196
|
+
const laneX = boundaryX(r.source.column, r.id);
|
|
197
|
+
guidesByEdgeId.set(r.id, {
|
|
198
|
+
...guide,
|
|
199
|
+
kind: "corridor",
|
|
200
|
+
laneX,
|
|
201
|
+
labelPoint: {
|
|
202
|
+
x: (sourceStubX + 6 * laneX + targetStubX) / 8,
|
|
203
|
+
y: (sourceY + targetY) / 2,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
const laneY =
|
|
208
|
+
r.side === "top"
|
|
209
|
+
? labelTop +
|
|
210
|
+
columnLabelHeight +
|
|
211
|
+
12 +
|
|
212
|
+
busSlots.top.slots.get(r.id) * PITCH
|
|
213
|
+
: bodyBottom +
|
|
214
|
+
topReserve +
|
|
215
|
+
24 +
|
|
216
|
+
busSlots.bottom.slots.get(r.id) * PITCH;
|
|
217
|
+
const sourceLaneX = boundaryX(r.source.column, `${r.id}:source`);
|
|
218
|
+
const targetLaneX = boundaryX(r.target.column - 1, `${r.id}:target`);
|
|
219
|
+
guidesByEdgeId.set(r.id, {
|
|
220
|
+
...guide,
|
|
221
|
+
kind: "bypass",
|
|
222
|
+
sourceLaneX,
|
|
223
|
+
targetLaneX,
|
|
224
|
+
laneY,
|
|
225
|
+
labelPoint: { x: (sourceLaneX + targetLaneX) / 2, y: laneY },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
const labels = columnLabelHeight
|
|
230
|
+
? columns.map((column, index) => ({
|
|
231
|
+
key: column.key,
|
|
232
|
+
roles: column.roles,
|
|
233
|
+
x: positions[index],
|
|
234
|
+
y: labelTop,
|
|
235
|
+
width: widths[index],
|
|
236
|
+
count: column.roles
|
|
237
|
+
? column.nodes.length
|
|
238
|
+
: column.nodes.filter((n) => n.role === column.key).length,
|
|
239
|
+
}))
|
|
240
|
+
: [];
|
|
241
|
+
indexed.forEach((n) => {
|
|
242
|
+
if (n.sectionStart)
|
|
243
|
+
labels.push({
|
|
244
|
+
key: n.role,
|
|
245
|
+
x: positions[n.column],
|
|
246
|
+
y: n.y + topReserve - 32,
|
|
247
|
+
width: widths[n.column],
|
|
248
|
+
count: columns[n.column].nodes.filter((item) => item.role === n.role)
|
|
249
|
+
.length,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
width:
|
|
254
|
+
positions[positions.length - 1] +
|
|
255
|
+
widths[widths.length - 1] +
|
|
256
|
+
outsideWidth(columns.length - 1),
|
|
257
|
+
height: bodyBottom + topReserve + bottomReserve,
|
|
258
|
+
geometries,
|
|
259
|
+
guidesByEdgeId: Object.fromEntries(guidesByEdgeId),
|
|
260
|
+
columns: labels,
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export const layoutInternalGroup = ({
|
|
265
|
+
children,
|
|
266
|
+
rankById,
|
|
267
|
+
edges,
|
|
268
|
+
dependencyLayout,
|
|
269
|
+
}) => {
|
|
270
|
+
const rankedColumns = new Map();
|
|
271
|
+
children.forEach((child) => {
|
|
272
|
+
const rank = rankById.get(child.id) || 0;
|
|
273
|
+
const column = rankedColumns.get(rank) || [];
|
|
274
|
+
column.push(child);
|
|
275
|
+
rankedColumns.set(rank, column);
|
|
276
|
+
});
|
|
277
|
+
const compact = dependencyLayout === "compact";
|
|
278
|
+
const roleColumns = internalRoleColumns(children, edges);
|
|
279
|
+
const rolesById = new Map(
|
|
280
|
+
roleColumns.flatMap(({ nodes }) => nodes.map(({ id, role }) => [id, role])),
|
|
281
|
+
);
|
|
282
|
+
const columns = compact
|
|
283
|
+
? [
|
|
284
|
+
{
|
|
285
|
+
key: roleColumns[0].key,
|
|
286
|
+
sectioned: true,
|
|
287
|
+
nodes: roleColumns.flatMap(({ nodes }) => nodes),
|
|
288
|
+
},
|
|
289
|
+
]
|
|
290
|
+
: [...rankedColumns].map(([rank, nodes]) => ({
|
|
291
|
+
rank,
|
|
292
|
+
key: `rank-${rank}`,
|
|
293
|
+
roles: [...new Set(nodes.map(({ id }) => rolesById.get(id)))],
|
|
294
|
+
nodes,
|
|
295
|
+
}));
|
|
296
|
+
return reserveInternalGroupChannels({
|
|
297
|
+
columns,
|
|
298
|
+
edges,
|
|
299
|
+
nodeGap: compact ? 12 : 8,
|
|
300
|
+
columnGap: compact ? 136 : 120,
|
|
301
|
+
columnLabelHeight: 24,
|
|
302
|
+
compact,
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export const applyInternalNodeLayers = (nodes = []) => {
|
|
307
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
308
|
+
const depths = new Map();
|
|
309
|
+
const depth = (id, visited = new Set()) => {
|
|
310
|
+
if (depths.has(id)) return depths.get(id);
|
|
311
|
+
const node = byId.get(id);
|
|
312
|
+
if (!node?.parentId || !byId.has(node.parentId) || visited.has(id))
|
|
313
|
+
return 0;
|
|
314
|
+
const value = 1 + depth(node.parentId, new Set([...visited, id]));
|
|
315
|
+
depths.set(id, value);
|
|
316
|
+
return value;
|
|
317
|
+
};
|
|
318
|
+
return nodes.map((node) =>
|
|
319
|
+
node.type === "lineageGroup"
|
|
320
|
+
? { ...node, zIndex: 10 + depth(node.id) }
|
|
321
|
+
: node.zIndex >= 200 ||
|
|
322
|
+
node.data?.internalEdgeGuides ||
|
|
323
|
+
byId.get(node.parentId)?.data?.internalEdgeGuides
|
|
324
|
+
? { ...node, zIndex: 200 + 4 * depth(node.id) }
|
|
325
|
+
: node,
|
|
326
|
+
);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const applyInternalEdgeRoutes = (edges = [], nodes = []) => {
|
|
330
|
+
const guides = new Map();
|
|
331
|
+
nodes.forEach((node) =>
|
|
332
|
+
Object.entries(node.data?.internalEdgeGuides || {}).forEach(([id, guide]) =>
|
|
333
|
+
guides.set(id, { node, guide }),
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
return edges.map((edge) => {
|
|
337
|
+
const owned = guides.get(edge.id);
|
|
338
|
+
if (!owned) {
|
|
339
|
+
if (!edge.data?.internalOwnerId) return edge;
|
|
340
|
+
return {
|
|
341
|
+
...edge,
|
|
342
|
+
zIndex: LINEAGE_PREVIEW_EDGE_Z_INDEX,
|
|
343
|
+
data: {
|
|
344
|
+
...edge.data,
|
|
345
|
+
selectionBaseZIndex: 100,
|
|
346
|
+
internalOwnerId: undefined,
|
|
347
|
+
routeGuide: undefined,
|
|
348
|
+
edgePath: "bezier",
|
|
349
|
+
lockTargetPort: false,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const { node, guide } = owned;
|
|
354
|
+
const { x, y } = node.data.absoluteGeometry;
|
|
355
|
+
const absolute = {
|
|
356
|
+
...guide,
|
|
357
|
+
...Object.fromEntries(
|
|
358
|
+
["laneX", "sourceLaneX", "targetLaneX", "sourceStubX", "targetStubX"]
|
|
359
|
+
.filter((key) => Number.isFinite(guide[key]))
|
|
360
|
+
.map((key) => [key, guide[key] + x]),
|
|
361
|
+
),
|
|
362
|
+
...(Number.isFinite(guide.laneY) ? { laneY: guide.laneY + y } : {}),
|
|
363
|
+
labelPoint: { x: guide.labelPoint.x + x, y: guide.labelPoint.y + y },
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
...edge,
|
|
367
|
+
zIndex: LINEAGE_PREVIEW_EDGE_Z_INDEX,
|
|
368
|
+
sourceHandle: "end-node",
|
|
369
|
+
targetHandle:
|
|
370
|
+
guide.targetPortPosition === "right"
|
|
371
|
+
? "target-right-node"
|
|
372
|
+
: "start-node",
|
|
373
|
+
data: {
|
|
374
|
+
...edge.data,
|
|
375
|
+
selectionBaseZIndex: node.zIndex + 2,
|
|
376
|
+
internalOwnerId: node.id,
|
|
377
|
+
edgePath: "guided-bezier",
|
|
378
|
+
routeGuide: absolute,
|
|
379
|
+
forceHorizontalPorts: true,
|
|
380
|
+
sourcePortPosition: guide.sourcePortPosition,
|
|
381
|
+
targetPortPosition: guide.targetPortPosition,
|
|
382
|
+
lockSourcePort: true,
|
|
383
|
+
lockTargetPort: true,
|
|
384
|
+
bundleSource: false,
|
|
385
|
+
bundleTarget: false,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { layoutInternalGroup } from "./lineageInternalRoutes";
|
|
2
|
+
|
|
1
3
|
const DEFAULT_NODE_WIDTH = 288;
|
|
2
4
|
const DEFAULT_NODE_HEIGHT = 82;
|
|
3
|
-
const COLUMN_GAP =
|
|
4
|
-
const NODE_GAP =
|
|
5
|
+
const COLUMN_GAP = 160;
|
|
6
|
+
const NODE_GAP = 48;
|
|
5
7
|
const FIELD_GAP = 4;
|
|
6
8
|
const ROOT_BRANCH_GAP = 40;
|
|
7
9
|
const GROUP_PADDING_X = 16;
|
|
@@ -861,7 +863,7 @@ const preserveAnchor = (geometries, previousLayout, anchorNodeId) => {
|
|
|
861
863
|
);
|
|
862
864
|
};
|
|
863
865
|
|
|
864
|
-
const applyGeometries = (nodes, geometries) =>
|
|
866
|
+
const applyGeometries = (nodes, geometries, internalLayouts) =>
|
|
865
867
|
nodes.map((node) => {
|
|
866
868
|
const geometry = geometries.get(node.id) || nodeGeometry(node);
|
|
867
869
|
const parentGeometry = node.parentId ? geometries.get(node.parentId) : null;
|
|
@@ -880,6 +882,8 @@ const applyGeometries = (nodes, geometries) =>
|
|
|
880
882
|
data: {
|
|
881
883
|
...node.data,
|
|
882
884
|
absoluteGeometry: geometry,
|
|
885
|
+
internalEdgeGuides: internalLayouts.get(node.id)?.guidesByEdgeId,
|
|
886
|
+
internalColumns: internalLayouts.get(node.id)?.columns,
|
|
883
887
|
},
|
|
884
888
|
style: {
|
|
885
889
|
...node.style,
|
|
@@ -889,7 +893,7 @@ const applyGeometries = (nodes, geometries) =>
|
|
|
889
893
|
};
|
|
890
894
|
});
|
|
891
895
|
|
|
892
|
-
const compoundGeometry = (nodes, edges) => {
|
|
896
|
+
const compoundGeometry = (nodes, edges, dependencyLayout, internalLayouts) => {
|
|
893
897
|
const hierarchy = buildHierarchy(nodes);
|
|
894
898
|
const { nodeById, parentById, childIdsByParentId } = hierarchy;
|
|
895
899
|
const ancestorsById = new Map();
|
|
@@ -946,13 +950,10 @@ const compoundGeometry = (nodes, edges) => {
|
|
|
946
950
|
(child) => nodeById.get(child).data?.variant === "lineage-field",
|
|
947
951
|
);
|
|
948
952
|
const childNodes = children.map((child) => nodeById.get(child));
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
fields ? [] : projectedByParent.get(id) || [],
|
|
952
|
-
"compound",
|
|
953
|
-
);
|
|
953
|
+
const relations = projectedByParent.get(id) || [];
|
|
954
|
+
const plan = cachedTopologyPlan(childNodes, relations, "compound");
|
|
954
955
|
const ordered = [...children].sort((a, b) => {
|
|
955
|
-
if (fields)
|
|
956
|
+
if (fields && !relations.length)
|
|
956
957
|
return (
|
|
957
958
|
nodeLabel(nodeById.get(a)).localeCompare(
|
|
958
959
|
nodeLabel(nodeById.get(b)),
|
|
@@ -964,6 +965,23 @@ const compoundGeometry = (nodes, edges) => {
|
|
|
964
965
|
a.localeCompare(b)
|
|
965
966
|
);
|
|
966
967
|
});
|
|
968
|
+
if (fields && relations.length) {
|
|
969
|
+
const block = layoutInternalGroup({
|
|
970
|
+
children: ordered.map((child) => {
|
|
971
|
+
const geometry = nodeGeometry(nodeById.get(child));
|
|
972
|
+
return { id: child, width: geometry.w, height: geometry.h };
|
|
973
|
+
}),
|
|
974
|
+
rankById: plan.rankById,
|
|
975
|
+
edges: relations,
|
|
976
|
+
dependencyLayout,
|
|
977
|
+
});
|
|
978
|
+
internalLayouts.set(id, {
|
|
979
|
+
guidesByEdgeId: block.guidesByEdgeId,
|
|
980
|
+
columns: block.columns,
|
|
981
|
+
});
|
|
982
|
+
block.geometries.set(id, { x: 0, y: 0, w: block.width, h: block.height });
|
|
983
|
+
return block;
|
|
984
|
+
}
|
|
967
985
|
const columns = new Map();
|
|
968
986
|
ordered.forEach((child) => {
|
|
969
987
|
const rank = fields ? 0 : plan.rankById.get(child) || 0;
|
|
@@ -1030,15 +1048,17 @@ export const layoutVisibleLineageGraph = ({
|
|
|
1030
1048
|
graphType = "impact",
|
|
1031
1049
|
nodes = [],
|
|
1032
1050
|
previousLayout = [],
|
|
1051
|
+
dependencyLayout = "compact",
|
|
1033
1052
|
}) => {
|
|
1034
1053
|
if (!nodes.length) return [];
|
|
1054
|
+
const internalLayouts = new Map();
|
|
1035
1055
|
|
|
1036
1056
|
const packed = nodes.some(
|
|
1037
1057
|
(node) =>
|
|
1038
1058
|
node.data?.topology ||
|
|
1039
1059
|
(node.parentId && node.data?.variant === "lineage-field"),
|
|
1040
1060
|
)
|
|
1041
|
-
? compoundGeometry(nodes, edges)
|
|
1061
|
+
? compoundGeometry(nodes, edges, dependencyLayout, internalLayouts)
|
|
1042
1062
|
: packedGeometry(
|
|
1043
1063
|
nodes,
|
|
1044
1064
|
cachedTopologyPlan(nodes, edges, graphType),
|
|
@@ -1046,5 +1066,5 @@ export const layoutVisibleLineageGraph = ({
|
|
|
1046
1066
|
);
|
|
1047
1067
|
const anchored = preserveAnchor(packed, previousLayout, anchorNodeId);
|
|
1048
1068
|
|
|
1049
|
-
return applyGeometries(nodes, anchored);
|
|
1069
|
+
return applyGeometries(nodes, anchored, internalLayouts);
|
|
1050
1070
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const KEY = "td.lineage.dependencyLayout.v1";
|
|
2
|
+
export const normalizeDependencyLayout = (value) =>
|
|
3
|
+
value === "expanded" ? "expanded" : "compact";
|
|
4
|
+
export const readDependencyLayout = () => {
|
|
5
|
+
try {
|
|
6
|
+
return normalizeDependencyLayout(globalThis.localStorage?.getItem(KEY));
|
|
7
|
+
} catch {
|
|
8
|
+
return "compact";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export const writeDependencyLayout = (value) => {
|
|
12
|
+
try {
|
|
13
|
+
const storage = globalThis.localStorage;
|
|
14
|
+
if (!storage) return false;
|
|
15
|
+
|
|
16
|
+
storage.setItem(KEY, normalizeDependencyLayout(value));
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|