@ocelescope/resources 0.0.0 → 0.1.0
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/LICENSE +661 -0
- package/README.md +19 -18
- package/dist/index.d.ts +33 -11
- package/dist/index.js +1710 -504
- package/package.json +18 -16
- package/dist/Cytoscape-BpEZfi09.js +0 -110
- package/dist/CytoscapeContext-DrvVpY9-.js +0 -10
package/dist/index.js
CHANGED
|
@@ -1,441 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { DownloadIcon, EyeOffIcon, Maximize, MaximizeIcon, MinusIcon, PlusIcon, Save, Settings, XIcon } from "lucide-react";
|
|
5
|
-
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
-
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { arrow, flip, offset, shift, useFloating } from "@floating-ui/react";
|
|
1
|
+
import { ActionIcon, Alert, Badge, Box, Button, ButtonGroup, Code, Group, LoadingOverlay, Menu, Modal, Popover, ScrollArea, Select, Stack, Text, ThemeIcon, Title } from "@mantine/core";
|
|
2
|
+
import { useResource, useResources } from "@ocelescope/api-base";
|
|
3
|
+
import { DownloadIcon, EyeOffIcon, Maximize2Icon, MaximizeIcon, MinusIcon, PlusIcon, XIcon } from "lucide-react";
|
|
8
4
|
import { Graphviz } from "@hpcc-js/wasm-graphviz";
|
|
9
|
-
import { isNil, omitBy } from "lodash";
|
|
10
|
-
import { DataTable } from "mantine-datatable";
|
|
11
|
-
import dayjs from "dayjs";
|
|
12
|
-
import isBetween from "dayjs/plugin/isBetween";
|
|
13
5
|
import { saveAs } from "file-saver";
|
|
6
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
14
7
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const cytoscape = context?.cy.current;
|
|
26
|
-
if (!cytoscape) return;
|
|
27
|
-
const pngData = cytoscape.png({
|
|
28
|
-
full: true,
|
|
29
|
-
scale: 2,
|
|
30
|
-
bg: "#ffffff"
|
|
31
|
-
});
|
|
32
|
-
const a = document.createElement("a");
|
|
33
|
-
a.href = pngData;
|
|
34
|
-
a.download = "graph.png";
|
|
35
|
-
a.dispatchEvent(new MouseEvent("click", {
|
|
36
|
-
bubbles: true,
|
|
37
|
-
cancelable: true,
|
|
38
|
-
view: window
|
|
39
|
-
}));
|
|
40
|
-
};
|
|
41
|
-
return /* @__PURE__ */ jsxs(ButtonGroup, {
|
|
42
|
-
style: { zIndex: 2 },
|
|
43
|
-
orientation: "vertical",
|
|
44
|
-
pos: "absolute",
|
|
45
|
-
bottom: 0,
|
|
46
|
-
left: 0,
|
|
47
|
-
children: [
|
|
48
|
-
/* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
49
|
-
disabled: !context,
|
|
50
|
-
px: "xs",
|
|
51
|
-
variant: "default",
|
|
52
|
-
onClick: context && (() => {
|
|
53
|
-
if (context.cy.current) context.cy.current.fit();
|
|
54
|
-
}),
|
|
55
|
-
children: /* @__PURE__ */ jsx(Maximize, { size: 18 })
|
|
56
|
-
}), /* @__PURE__ */ jsx(Button, {
|
|
57
|
-
disabled: !context,
|
|
58
|
-
onClick: downloadGraphAsPng,
|
|
59
|
-
px: "xs",
|
|
60
|
-
variant: "default",
|
|
61
|
-
children: /* @__PURE__ */ jsx(DownloadIcon, { size: 18 })
|
|
62
|
-
})] }),
|
|
63
|
-
toggleOptions && /* @__PURE__ */ jsx(Button, {
|
|
64
|
-
onClick: toggleOptions,
|
|
65
|
-
px: "xs",
|
|
66
|
-
variant: "default",
|
|
67
|
-
children: /* @__PURE__ */ jsx(Settings, { size: 18 })
|
|
68
|
-
}),
|
|
69
|
-
onSave && /* @__PURE__ */ jsx(Button, {
|
|
70
|
-
onClick: onSave,
|
|
71
|
-
px: "xs",
|
|
72
|
-
variant: "default",
|
|
73
|
-
children: /* @__PURE__ */ jsx(Save, { size: 18 })
|
|
74
|
-
})
|
|
75
|
-
]
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
var ActionButtons_default = ActionButtons;
|
|
79
|
-
|
|
80
|
-
//#endregion
|
|
81
|
-
//#region src/components/Cytoscape/hooks/useEntityClick.ts
|
|
82
|
-
const triggerActionToStartEvent = {
|
|
83
|
-
hover: "tapdragover",
|
|
84
|
-
rightClick: "cxttap",
|
|
85
|
-
leftClick: "tap",
|
|
86
|
-
doubleClick: "dbltap"
|
|
87
|
-
};
|
|
88
|
-
const useEntityClick = ({ trigger }) => {
|
|
89
|
-
const context = useCytoscapeContext();
|
|
90
|
-
const [entity, setEntity] = useState();
|
|
91
|
-
const containerRef = useRef(null);
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
const cytoscape = context?.cy.current;
|
|
94
|
-
const container = cytoscape?.container();
|
|
95
|
-
if (!cytoscape || !container) return;
|
|
96
|
-
containerRef.current = container;
|
|
97
|
-
const handleEntitySelect = (event) => {
|
|
98
|
-
const target = event.target;
|
|
99
|
-
if (target.isNode()) setEntity({
|
|
100
|
-
type: "node",
|
|
101
|
-
id: target.id(),
|
|
102
|
-
boundingBox: target.renderedBoundingBox()
|
|
103
|
-
});
|
|
104
|
-
else if (target.isEdge()) {
|
|
105
|
-
const midpoint = target.renderedMidpoint();
|
|
106
|
-
setEntity({
|
|
107
|
-
type: "edge",
|
|
108
|
-
id: target.id(),
|
|
109
|
-
midpoint: {
|
|
110
|
-
x: midpoint.x,
|
|
111
|
-
y: midpoint.y
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
const handleClickOutside = (e) => {
|
|
117
|
-
if (e.target === context?.cy.current) setEntity(void 0);
|
|
118
|
-
};
|
|
119
|
-
const handleViewChange = () => {
|
|
120
|
-
setEntity((prev) => {
|
|
121
|
-
if (!prev) return prev;
|
|
122
|
-
const ele = context?.cy.current?.getElementById(prev.id);
|
|
123
|
-
if (!ele || ele.empty()) return prev;
|
|
124
|
-
if (prev.type === "node") return {
|
|
125
|
-
...prev,
|
|
126
|
-
boundingBox: ele.renderedBoundingBox()
|
|
127
|
-
};
|
|
128
|
-
const midpoint = ele.renderedMidpoint();
|
|
129
|
-
return {
|
|
130
|
-
...prev,
|
|
131
|
-
midpoint
|
|
132
|
-
};
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
cytoscape.on(triggerActionToStartEvent[trigger], "node, edge", handleEntitySelect);
|
|
136
|
-
cytoscape.on("pan zoom position", handleViewChange);
|
|
137
|
-
cytoscape.on("tap", handleClickOutside);
|
|
138
|
-
return () => {
|
|
139
|
-
cytoscape.removeListener(triggerActionToStartEvent[trigger], "node, edge", handleEntitySelect);
|
|
140
|
-
cytoscape.removeListener("pan zoom position", handleViewChange);
|
|
141
|
-
cytoscape.removeListener("tap", handleClickOutside);
|
|
142
|
-
};
|
|
143
|
-
}, [context, trigger]);
|
|
144
|
-
return {
|
|
145
|
-
entity,
|
|
146
|
-
container: containerRef,
|
|
147
|
-
resetEntity: () => setEntity(void 0)
|
|
148
|
-
};
|
|
149
|
-
};
|
|
150
|
-
var useEntityClick_default = useEntityClick;
|
|
151
|
-
|
|
152
|
-
//#endregion
|
|
153
|
-
//#region src/components/Cytoscape/components/FloatingAnnotation.tsx
|
|
154
|
-
const FloatingAnnotation = ({ trigger = "leftClick", children }) => {
|
|
155
|
-
const { entity, container } = useEntityClick_default({ trigger });
|
|
156
|
-
const arrowRef = useRef(null);
|
|
157
|
-
const virtualRef = useMemo(() => {
|
|
158
|
-
const containerRect = container.current?.getBoundingClientRect();
|
|
159
|
-
if (!entity || !containerRect) return;
|
|
160
|
-
if (entity.type === "node") {
|
|
161
|
-
const b = entity.boundingBox;
|
|
162
|
-
const width = b.x2 - b.x1;
|
|
163
|
-
const height = b.y2 - b.y1;
|
|
164
|
-
const top = containerRect.top + b.y1;
|
|
165
|
-
const left = containerRect.left + b.x1;
|
|
166
|
-
return { getBoundingClientRect: () => ({
|
|
167
|
-
x: left,
|
|
168
|
-
y: top,
|
|
169
|
-
width,
|
|
170
|
-
height,
|
|
171
|
-
top,
|
|
172
|
-
left,
|
|
173
|
-
right: left + width,
|
|
174
|
-
bottom: top + height
|
|
175
|
-
}) };
|
|
176
|
-
}
|
|
177
|
-
const m = entity.midpoint;
|
|
178
|
-
const x$1 = containerRect.left + m.x;
|
|
179
|
-
const y$1 = containerRect.top + m.y;
|
|
180
|
-
return { getBoundingClientRect: () => ({
|
|
181
|
-
x: x$1,
|
|
182
|
-
y: y$1,
|
|
183
|
-
width: 0,
|
|
184
|
-
height: 0,
|
|
185
|
-
top: y$1,
|
|
186
|
-
left: x$1,
|
|
187
|
-
right: x$1,
|
|
188
|
-
bottom: y$1
|
|
189
|
-
}) };
|
|
190
|
-
}, [entity, container]);
|
|
191
|
-
const { x, y, refs, strategy } = useFloating({
|
|
192
|
-
placement: "right",
|
|
193
|
-
strategy: "fixed",
|
|
194
|
-
middleware: [
|
|
195
|
-
flip(),
|
|
196
|
-
offset(10),
|
|
197
|
-
shift({ padding: 8 }),
|
|
198
|
-
arrow({ element: arrowRef })
|
|
199
|
-
]
|
|
200
|
-
});
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
refs.setReference(virtualRef);
|
|
203
|
-
}, [virtualRef, refs]);
|
|
204
|
-
const child = useMemo(() => entity ? children(entity) : void 0, [entity, children]);
|
|
205
|
-
if (!entity || !virtualRef || child == null) return null;
|
|
206
|
-
return /* @__PURE__ */ jsx(Paper, {
|
|
207
|
-
ref: refs.setFloating,
|
|
208
|
-
shadow: "md",
|
|
209
|
-
radius: "md",
|
|
210
|
-
p: "md",
|
|
211
|
-
withBorder: true,
|
|
212
|
-
style: {
|
|
213
|
-
position: strategy,
|
|
214
|
-
top: y ?? 0,
|
|
215
|
-
left: x ?? 0,
|
|
216
|
-
width: "min(90vw, 400px)",
|
|
217
|
-
height: "min(80vh, 300px)",
|
|
218
|
-
overflow: "auto",
|
|
219
|
-
zIndex: 9999
|
|
220
|
-
},
|
|
221
|
-
children: child
|
|
222
|
-
});
|
|
223
|
-
};
|
|
224
|
-
var FloatingAnnotation_default = FloatingAnnotation;
|
|
225
|
-
|
|
226
|
-
//#endregion
|
|
227
|
-
//#region src/hooks/useGraphvizLayout.ts
|
|
228
|
-
const esc = (val) => {
|
|
229
|
-
return val.replaceAll("\"", "\\\"");
|
|
230
|
-
};
|
|
231
|
-
const safeId = (id) => id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
232
|
-
const attrsToDot = (obj) => {
|
|
233
|
-
if (!obj) return "";
|
|
234
|
-
return Object.entries(obj).filter(([_, v]) => v !== null && v !== void 0).map(([k, v]) => typeof v === "string" ? `${k}="${esc(v)}"` : `${k}=${String(v)}`).join(",");
|
|
235
|
-
};
|
|
236
|
-
const toDot = (visualization) => {
|
|
237
|
-
const gAttrs = attrsToDot(visualization.layout_config?.graphAttrs ?? void 0);
|
|
238
|
-
const nAttrs = attrsToDot(visualization.layout_config?.nodeAttrs ?? void 0);
|
|
239
|
-
const eAttrs = attrsToDot(visualization.layout_config?.edgeAttrs ?? void 0);
|
|
240
|
-
const lines = [
|
|
241
|
-
"digraph G {",
|
|
242
|
-
`graph [${gAttrs}];`,
|
|
243
|
-
`node [${nAttrs}];`,
|
|
244
|
-
`edge [${eAttrs}];`
|
|
245
|
-
];
|
|
246
|
-
for (const node of visualization.nodes ?? []) {
|
|
247
|
-
const id = safeId(node?.id ?? "");
|
|
248
|
-
const attrs = {
|
|
249
|
-
...node.layout_attrs,
|
|
250
|
-
label: node.label ? esc(node.label) : void 0,
|
|
251
|
-
shape: node.shape,
|
|
252
|
-
color: node.color,
|
|
253
|
-
...node.width && { width: (node.width / 72).toFixed(4) },
|
|
254
|
-
...node.height && { height: (node.height / 72).toFixed(4) }
|
|
255
|
-
};
|
|
256
|
-
lines.push(`"${id}" [${attrsToDot(attrs)}];`);
|
|
257
|
-
}
|
|
258
|
-
for (const edge of visualization.edges ?? []) {
|
|
259
|
-
const attrs = {
|
|
260
|
-
...edge.layout_attrs,
|
|
261
|
-
label: edge.label ? esc(edge.label) : void 0,
|
|
262
|
-
color: edge.color
|
|
263
|
-
};
|
|
264
|
-
lines.push(`"${esc(safeId(edge.source))}" -> "${esc(safeId(edge.target))}" [${attrsToDot(attrs)}];`);
|
|
265
|
-
}
|
|
266
|
-
const ranks = (visualization.nodes ?? []).filter(({ rank }) => rank != null).reduce((acc, current) => {
|
|
267
|
-
const key = current.rank;
|
|
268
|
-
acc[key] ??= [];
|
|
269
|
-
acc[key].push(esc(safeId(current?.id ?? "")));
|
|
270
|
-
return acc;
|
|
271
|
-
}, {});
|
|
272
|
-
lines.push(...Object.entries(ranks).map(([key, value]) => `{ rank=${["source", "sink"].includes(key) ? key : "same"}; ${(value ?? []).join(" ")} } `));
|
|
273
|
-
lines.push("}");
|
|
274
|
-
return lines.join("\n");
|
|
275
|
-
};
|
|
276
|
-
const parsePos = (s) => {
|
|
277
|
-
if (s) {
|
|
278
|
-
const [xs, ys] = s.split(",");
|
|
279
|
-
const x = Number.parseFloat(xs ?? "0");
|
|
280
|
-
const y = Number.parseFloat(ys ?? "0");
|
|
281
|
-
if (Number.isFinite(x) && Number.isFinite(y)) return {
|
|
282
|
-
x,
|
|
283
|
-
y
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
return {
|
|
287
|
-
x: 0,
|
|
288
|
-
y: 0
|
|
289
|
-
};
|
|
290
|
-
};
|
|
291
|
-
const useGraphvizLayout = (visualization) => {
|
|
292
|
-
const [elements, setElements] = useState(null);
|
|
293
|
-
const [error, setError] = useState(null);
|
|
294
|
-
const cancelRef = useRef({ cancelled: false });
|
|
295
|
-
const dot = useMemo(() => toDot(visualization), [visualization]);
|
|
296
|
-
useEffect(() => {
|
|
297
|
-
cancelRef.current.cancelled = false;
|
|
298
|
-
setError(null);
|
|
299
|
-
(async () => {
|
|
300
|
-
try {
|
|
301
|
-
const jsonStr = (await Graphviz.load()).layout(dot, "json", visualization.layout_config?.engine ?? "dot");
|
|
302
|
-
const gvJson = JSON.parse(jsonStr);
|
|
303
|
-
const nodePos = {};
|
|
304
|
-
for (const o of gvJson.objects ?? []) {
|
|
305
|
-
if (!o.name || !o.pos) return;
|
|
306
|
-
const p = parsePos(o.pos);
|
|
307
|
-
if (!p) return;
|
|
308
|
-
nodePos[o.name] = {
|
|
309
|
-
...p,
|
|
310
|
-
width: o.width ? Number.parseFloat(o.width) * 72 : 0
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
const nodes = (visualization.nodes ?? []).map((node) => ({
|
|
314
|
-
data: {
|
|
315
|
-
...omitBy(node, isNil),
|
|
316
|
-
label: node.label ?? "",
|
|
317
|
-
shape: node.shape !== "circle" ? node.shape : "ellipse",
|
|
318
|
-
width: node.width ?? nodePos[safeId(node.id ?? "")]?.width,
|
|
319
|
-
height: node.height ?? 30
|
|
320
|
-
},
|
|
321
|
-
position: nodePos[safeId(node.id ?? "")] ?? {
|
|
322
|
-
x: 0,
|
|
323
|
-
y: 0
|
|
324
|
-
}
|
|
325
|
-
}));
|
|
326
|
-
const edges = (visualization.edges ?? []).map((edge) => ({ data: omitBy(edge, isNil) }));
|
|
327
|
-
if (!cancelRef.current.cancelled) setElements([...nodes, ...edges]);
|
|
328
|
-
} catch (e) {
|
|
329
|
-
if (!cancelRef.current.cancelled) {
|
|
330
|
-
setError(e?.message ?? "Graphviz layout failed.");
|
|
331
|
-
setElements(null);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
})();
|
|
335
|
-
return () => {
|
|
336
|
-
cancelRef.current.cancelled = true;
|
|
337
|
-
};
|
|
338
|
-
}, [dot, visualization]);
|
|
339
|
-
return {
|
|
340
|
-
elements,
|
|
341
|
-
error
|
|
342
|
-
};
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
//#endregion
|
|
346
|
-
//#region src/components/ResourceView/Viewers/Graph.tsx
|
|
347
|
-
const GraphViewer = ({ visualization, isPreview }) => {
|
|
348
|
-
const { elements } = useGraphvizLayout(visualization);
|
|
349
|
-
return /* @__PURE__ */ jsx(Box, {
|
|
350
|
-
h: "100%",
|
|
351
|
-
w: "100%",
|
|
352
|
-
pos: "relative",
|
|
353
|
-
style: { overflow: "hidden" },
|
|
354
|
-
children: elements && /* @__PURE__ */ jsx(Cytoscape_default, {
|
|
355
|
-
userZoomingEnabled: !isPreview,
|
|
356
|
-
userPanningEnabled: !isPreview,
|
|
357
|
-
style: {
|
|
358
|
-
width: "100%",
|
|
359
|
-
height: "100%"
|
|
360
|
-
},
|
|
361
|
-
elements,
|
|
362
|
-
layout: { name: "preset" },
|
|
363
|
-
children: !isPreview && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActionButtons_default, {}), /* @__PURE__ */ jsx(FloatingAnnotation_default, { children: (entity) => {
|
|
364
|
-
const visualizationEntity = visualization[entity.type === "edge" ? "edges" : "nodes"]?.find(({ id }) => id === entity.id);
|
|
365
|
-
if (!visualizationEntity?.annotation) return;
|
|
366
|
-
return /* @__PURE__ */ jsx(Visualization, {
|
|
367
|
-
visualization: visualizationEntity.annotation,
|
|
368
|
-
isPreview: true
|
|
369
|
-
});
|
|
370
|
-
} })] })
|
|
371
|
-
})
|
|
372
|
-
});
|
|
373
|
-
};
|
|
374
|
-
var Graph_default = GraphViewer;
|
|
375
|
-
|
|
376
|
-
//#endregion
|
|
377
|
-
//#region src/util/dayjs.ts
|
|
378
|
-
dayjs.extend(isBetween);
|
|
379
|
-
var dayjs_default = dayjs;
|
|
380
|
-
const formatDateTime = (dateString) => dayjs(dateString).format("YYYY-MM-DD HH:mm");
|
|
381
|
-
const formatDate = (dateString) => dayjs(dateString).format("YYYY-MM-DD");
|
|
382
|
-
|
|
383
|
-
//#endregion
|
|
384
|
-
//#region src/components/ResourceView/Viewers/Table.tsx
|
|
385
|
-
const formatCell = (value, dataType) => {
|
|
386
|
-
if (value == null) return "";
|
|
387
|
-
switch (dataType) {
|
|
388
|
-
case "boolean": return value ? "✅" : "❌";
|
|
389
|
-
case "date": return dayjs_default(value).isValid() ? formatDate(value) : String(value);
|
|
390
|
-
case "datetime": return dayjs_default(value).isValid() ? formatDateTime(value) : String(value);
|
|
391
|
-
default: return String(value);
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
const PAGE_SIZES = [
|
|
395
|
-
20,
|
|
396
|
-
50,
|
|
397
|
-
75
|
|
398
|
-
];
|
|
399
|
-
const TableView = ({ visualization }) => {
|
|
400
|
-
const [pageSize, setPageSize] = useState(PAGE_SIZES[0]);
|
|
401
|
-
const columns = useMemo(() => visualization.columns.map((col) => ({
|
|
402
|
-
accessor: col.id,
|
|
403
|
-
title: col.label ?? col.id,
|
|
404
|
-
render: (record) => formatCell(record[col.id], col.data_type)
|
|
405
|
-
})), [visualization.columns]);
|
|
406
|
-
const [page, setPage] = useState(1);
|
|
407
|
-
const [records, setRecords] = useState(visualization.rows.slice(0, pageSize));
|
|
408
|
-
useEffect(() => {
|
|
409
|
-
const from = (page - 1) * pageSize;
|
|
410
|
-
const to = from + pageSize;
|
|
411
|
-
setRecords(visualization.rows.slice(from, to));
|
|
412
|
-
}, [
|
|
413
|
-
page,
|
|
414
|
-
pageSize,
|
|
415
|
-
visualization
|
|
416
|
-
]);
|
|
417
|
-
return /* @__PURE__ */ jsx(DataTable, {
|
|
418
|
-
records,
|
|
419
|
-
columns,
|
|
420
|
-
withTableBorder: true,
|
|
421
|
-
withColumnBorders: true,
|
|
422
|
-
withRowBorders: true,
|
|
423
|
-
...visualization.rows.length > PAGE_SIZES[0] ? {
|
|
424
|
-
totalRecords: visualization.rows.length,
|
|
425
|
-
recordsPerPage: pageSize,
|
|
426
|
-
onPageChange: (p) => setPage(p),
|
|
427
|
-
page,
|
|
428
|
-
recordsPerPageOptions: [...PAGE_SIZES],
|
|
429
|
-
onRecordsPerPageChange: (newPageSize) => {
|
|
430
|
-
setPageSize(newPageSize);
|
|
431
|
-
setPage(1);
|
|
432
|
-
}
|
|
433
|
-
} : {}
|
|
434
|
-
});
|
|
435
|
-
};
|
|
436
|
-
var Table_default = TableView;
|
|
437
|
-
|
|
438
|
-
//#endregion
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { Background, BaseEdge, ControlButton, Controls, EdgeLabelRenderer, Handle, Position, ReactFlow, ReactFlowProvider, applyNodeChanges, getBezierPath, getSmoothStepPath, useInternalNode, useNodes, useNodesInitialized, useReactFlow } from "@xyflow/react";
|
|
10
|
+
import "@xyflow/react/dist/style.css";
|
|
11
|
+
import ELK from "elkjs/lib/elk.bundled.js";
|
|
12
|
+
import { toPng, toSvg } from "html-to-image";
|
|
13
|
+
import { jsPDF } from "jspdf";
|
|
14
|
+
import dynamic from "next/dynamic.js";
|
|
15
|
+
import { DataTable } from "mantine-datatable";
|
|
16
|
+
import dayjs from "dayjs";
|
|
17
|
+
import isBetween from "dayjs/plugin/isBetween.js";
|
|
439
18
|
//#region src/components/ResourceView/Viewers/SVG.tsx
|
|
440
19
|
const SvgPanWrapper = ({ children, onDownload }) => {
|
|
441
20
|
return /* @__PURE__ */ jsx(Box, {
|
|
@@ -447,7 +26,7 @@ const SvgPanWrapper = ({ children, onDownload }) => {
|
|
|
447
26
|
minScale: .1,
|
|
448
27
|
maxScale: 10,
|
|
449
28
|
centerOnInit: true,
|
|
450
|
-
children: ({ zoomIn, zoomOut, resetTransform }) => /* @__PURE__ */ jsxs(Fragment
|
|
29
|
+
children: ({ zoomIn, zoomOut, resetTransform }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TransformComponent, {
|
|
451
30
|
wrapperStyle: {
|
|
452
31
|
width: "100%",
|
|
453
32
|
height: "100%"
|
|
@@ -464,24 +43,18 @@ const SvgPanWrapper = ({ children, onDownload }) => {
|
|
|
464
43
|
children: [
|
|
465
44
|
/* @__PURE__ */ jsx(Button, {
|
|
466
45
|
px: "xs",
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
onClick: () => resetTransform(0)
|
|
470
|
-
})
|
|
46
|
+
onClick: () => resetTransform(0),
|
|
47
|
+
children: /* @__PURE__ */ jsx(MaximizeIcon, { width: 18 })
|
|
471
48
|
}),
|
|
472
49
|
/* @__PURE__ */ jsx(Button, {
|
|
473
50
|
px: "xs",
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
onClick: () => zoomIn()
|
|
477
|
-
})
|
|
51
|
+
onClick: () => zoomIn(),
|
|
52
|
+
children: /* @__PURE__ */ jsx(PlusIcon, { width: 18 })
|
|
478
53
|
}),
|
|
479
54
|
/* @__PURE__ */ jsx(Button, {
|
|
480
55
|
px: "xs",
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
onClick: () => zoomOut()
|
|
484
|
-
})
|
|
56
|
+
onClick: () => zoomOut(),
|
|
57
|
+
children: /* @__PURE__ */ jsx(MinusIcon, { width: 18 })
|
|
485
58
|
}),
|
|
486
59
|
/* @__PURE__ */ jsx(Button, {
|
|
487
60
|
px: "xs",
|
|
@@ -495,24 +68,23 @@ const SvgPanWrapper = ({ children, onDownload }) => {
|
|
|
495
68
|
};
|
|
496
69
|
const SvgViewer = ({ visualization, isPreview }) => {
|
|
497
70
|
const cleanedSvg = visualization.svg.replace(/width="[^"]+"/, "width=\"100%\"").replace(/height="[^"]+"/, "height=\"100%\"").replace(/<svg(?![^>]*xmlns=)/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
|
|
498
|
-
const Wrapper = isPreview ? Fragment : SvgPanWrapper;
|
|
499
71
|
const handleDownload = () => {
|
|
500
72
|
if (!cleanedSvg) return;
|
|
501
73
|
saveAs(new Blob([cleanedSvg], { type: "image/svg+xml;charset=utf-8" }), "resource.svg");
|
|
502
74
|
};
|
|
503
|
-
|
|
75
|
+
const content = /* @__PURE__ */ jsx("div", {
|
|
76
|
+
style: {
|
|
77
|
+
width: "100%",
|
|
78
|
+
height: "100%"
|
|
79
|
+
},
|
|
80
|
+
dangerouslySetInnerHTML: { __html: cleanedSvg }
|
|
81
|
+
});
|
|
82
|
+
if (isPreview) return content;
|
|
83
|
+
return /* @__PURE__ */ jsx(SvgPanWrapper, {
|
|
504
84
|
onDownload: handleDownload,
|
|
505
|
-
children:
|
|
506
|
-
style: {
|
|
507
|
-
width: "100%",
|
|
508
|
-
height: "100%"
|
|
509
|
-
},
|
|
510
|
-
dangerouslySetInnerHTML: { __html: cleanedSvg }
|
|
511
|
-
})
|
|
85
|
+
children: content
|
|
512
86
|
});
|
|
513
87
|
};
|
|
514
|
-
var SVG_default = SvgViewer;
|
|
515
|
-
|
|
516
88
|
//#endregion
|
|
517
89
|
//#region src/components/ResourceView/Viewers/Dot.tsx
|
|
518
90
|
const DotToSvgViewer = ({ isPreview, visualization }) => {
|
|
@@ -545,45 +117,1610 @@ const DotToSvgViewer = ({ isPreview, visualization }) => {
|
|
|
545
117
|
if (!cleanedSvg) return;
|
|
546
118
|
saveAs(new Blob([cleanedSvg], { type: "image/svg+xml;charset=utf-8" }), "resource.svg");
|
|
547
119
|
};
|
|
548
|
-
|
|
120
|
+
const content = /* @__PURE__ */ jsxs("div", {
|
|
121
|
+
style: {
|
|
122
|
+
width: "100%",
|
|
123
|
+
height: "100%",
|
|
124
|
+
position: "relative"
|
|
125
|
+
},
|
|
126
|
+
children: [error && /* @__PURE__ */ jsxs("div", {
|
|
127
|
+
style: {
|
|
128
|
+
position: "absolute",
|
|
129
|
+
top: 8,
|
|
130
|
+
left: 8,
|
|
131
|
+
color: "red",
|
|
132
|
+
zIndex: 10
|
|
133
|
+
},
|
|
134
|
+
children: ["Graphviz error: ", error]
|
|
135
|
+
}), cleanedSvg ? /* @__PURE__ */ jsx("div", {
|
|
136
|
+
style: {
|
|
137
|
+
width: "100%",
|
|
138
|
+
height: "100%"
|
|
139
|
+
},
|
|
140
|
+
dangerouslySetInnerHTML: { __html: cleanedSvg }
|
|
141
|
+
}) : !error && /* @__PURE__ */ jsx("div", {
|
|
142
|
+
style: {
|
|
143
|
+
width: "100%",
|
|
144
|
+
height: "100%",
|
|
145
|
+
display: "grid",
|
|
146
|
+
placeItems: "center",
|
|
147
|
+
opacity: .6,
|
|
148
|
+
fontSize: 14
|
|
149
|
+
},
|
|
150
|
+
children: "Rendering…"
|
|
151
|
+
})]
|
|
152
|
+
});
|
|
153
|
+
if (isPreview) return content;
|
|
154
|
+
return /* @__PURE__ */ jsx(SvgPanWrapper, {
|
|
549
155
|
onDownload: handleDownload,
|
|
550
|
-
children:
|
|
156
|
+
children: content
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/model/types.ts
|
|
161
|
+
var GraphVisualizationError = class extends Error {
|
|
162
|
+
details;
|
|
163
|
+
constructor(message, details) {
|
|
164
|
+
super(message);
|
|
165
|
+
this.name = "GraphVisualizationError";
|
|
166
|
+
this.details = details;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const DEFAULT_COLORS = {
|
|
170
|
+
edge: "#555555",
|
|
171
|
+
place: "#aec6e8",
|
|
172
|
+
transition: "#ffffff",
|
|
173
|
+
transitionBorder: "#333",
|
|
174
|
+
text: "#1a1a1a"
|
|
175
|
+
};
|
|
176
|
+
const FIT_VIEW_PADDING = .15;
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/model/buildModel.ts
|
|
179
|
+
const EDGE_ROUTINGS = /* @__PURE__ */ new Set([
|
|
180
|
+
"SPLINES",
|
|
181
|
+
"ORTHOGONAL",
|
|
182
|
+
"POLYLINE"
|
|
183
|
+
]);
|
|
184
|
+
const EDGE_ROUTING_KEYS = ["elk.edgeRouting", "org.eclipse.elk.edgeRouting"];
|
|
185
|
+
const normalizeEdgeRouting = (value) => {
|
|
186
|
+
if (typeof value !== "string") throw new GraphVisualizationError("Invalid ELK edge routing.", [`Expected one of ${Array.from(EDGE_ROUTINGS).join(", ")}, got ${String(value)}.`]);
|
|
187
|
+
const normalized = value.toUpperCase();
|
|
188
|
+
if (!EDGE_ROUTINGS.has(normalized)) throw new GraphVisualizationError("Invalid ELK edge routing.", [`Unknown edge routing "${value}". Supported values are ${Array.from(EDGE_ROUTINGS).join(", ")}.`]);
|
|
189
|
+
return normalized;
|
|
190
|
+
};
|
|
191
|
+
const normalizeElkOptions = (elkOptions) => {
|
|
192
|
+
const edgeRouting = EDGE_ROUTING_KEYS.flatMap((key) => key in elkOptions ? [normalizeEdgeRouting(elkOptions[key])] : [])[0] ?? "POLYLINE";
|
|
193
|
+
const normalizedOptions = { ...elkOptions };
|
|
194
|
+
for (const key of EDGE_ROUTING_KEYS) if (key in normalizedOptions) normalizedOptions[key] = edgeRouting;
|
|
195
|
+
return {
|
|
196
|
+
edgeRouting,
|
|
197
|
+
elkOptions: normalizedOptions
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
const mapNode = (node) => ({
|
|
201
|
+
id: node.id,
|
|
202
|
+
type: "node",
|
|
203
|
+
position: {
|
|
204
|
+
x: node.x ?? 0,
|
|
205
|
+
y: node.y ?? 0
|
|
206
|
+
},
|
|
207
|
+
data: node
|
|
208
|
+
});
|
|
209
|
+
const mapEdge = (edge) => ({
|
|
210
|
+
id: edge.id,
|
|
211
|
+
source: edge.source,
|
|
212
|
+
target: edge.target,
|
|
213
|
+
type: "graphflow",
|
|
214
|
+
data: edge
|
|
215
|
+
});
|
|
216
|
+
const createLayoutPlan = (visualization) => {
|
|
217
|
+
const layoutConfig = visualization.layout_config;
|
|
218
|
+
switch (layoutConfig?.type) {
|
|
219
|
+
case "graphviz":
|
|
220
|
+
if (!layoutConfig.engine) throw new GraphVisualizationError("Invalid Graphviz layout config.", ["Missing Graphviz layout engine."]);
|
|
221
|
+
return {
|
|
222
|
+
type: "graphviz",
|
|
223
|
+
engine: layoutConfig.engine,
|
|
224
|
+
graphAttrs: layoutConfig.graphAttrs ?? {},
|
|
225
|
+
nodeAttrs: layoutConfig.nodeAttrs ?? {},
|
|
226
|
+
edgeAttrs: layoutConfig.edgeAttrs ?? {}
|
|
227
|
+
};
|
|
228
|
+
case "elk": {
|
|
229
|
+
const normalized = normalizeElkOptions(layoutConfig.options ?? {});
|
|
230
|
+
return {
|
|
231
|
+
type: "elk",
|
|
232
|
+
elkOptions: normalized.elkOptions,
|
|
233
|
+
edgeRouting: normalized.edgeRouting
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
default: throw new GraphVisualizationError("Invalid graph layout config.", ["Expected an ELK or Graphviz layout config from the backend."]);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const buildGraphFlowModel = (visualization) => ({
|
|
240
|
+
nodes: (visualization.nodes ?? []).map(mapNode),
|
|
241
|
+
edges: (visualization.edges ?? []).map(mapEdge),
|
|
242
|
+
layoutPlan: createLayoutPlan(visualization)
|
|
243
|
+
});
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/model/runLayout.ts
|
|
246
|
+
const toGraphError = (error) => {
|
|
247
|
+
if (error instanceof GraphVisualizationError) return error;
|
|
248
|
+
return new GraphVisualizationError("Graph layout failed.", [error instanceof Error ? error.message : String(error)]);
|
|
249
|
+
};
|
|
250
|
+
const measuredNodesMatchModel = (measuredNodes, model) => {
|
|
251
|
+
if (measuredNodes.length !== model.nodes.length) return false;
|
|
252
|
+
const measuredNodeIds = new Set(measuredNodes.map((node) => node.id));
|
|
253
|
+
return model.nodes.every((node) => measuredNodeIds.has(node.id));
|
|
254
|
+
};
|
|
255
|
+
const applyNodePositions = (nodes, positions) => nodes.map((node) => {
|
|
256
|
+
const position = positions[node.id];
|
|
257
|
+
return position ? {
|
|
258
|
+
...node,
|
|
259
|
+
position
|
|
260
|
+
} : node;
|
|
261
|
+
});
|
|
262
|
+
const applyEdgeLayouts = (edges, edgeLayouts) => edges.map((edge) => {
|
|
263
|
+
const layout = edgeLayouts[edge.id];
|
|
264
|
+
if (!layout) return edge;
|
|
265
|
+
return {
|
|
266
|
+
...edge,
|
|
267
|
+
data: {
|
|
268
|
+
...edge.data,
|
|
269
|
+
path: layout.path,
|
|
270
|
+
labelPosition: layout.labelPosition,
|
|
271
|
+
startPoint: layout.startPoint,
|
|
272
|
+
endPoint: layout.endPoint
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/useElkLayout.ts
|
|
278
|
+
const buildPolylinePath$1 = (points) => {
|
|
279
|
+
if (points.length < 2) return null;
|
|
280
|
+
return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x},${point.y}`).join(" ");
|
|
281
|
+
};
|
|
282
|
+
const buildSplinePath$1 = (section) => {
|
|
283
|
+
const start = section.startPoint;
|
|
284
|
+
const end = section.endPoint;
|
|
285
|
+
if (!start || !end) return null;
|
|
286
|
+
const bendPoints = section.bendPoints ?? [];
|
|
287
|
+
if (bendPoints.length === 0) return `M ${start.x},${start.y} L ${end.x},${end.y}`;
|
|
288
|
+
let path = `M ${start.x},${start.y}`;
|
|
289
|
+
for (let index = 0; index < bendPoints.length; index += 3) {
|
|
290
|
+
const cp1 = bendPoints[index];
|
|
291
|
+
const cp2 = bendPoints[index + 1];
|
|
292
|
+
if (!cp1 || !cp2) return null;
|
|
293
|
+
const anchor = bendPoints[index + 2] ?? end;
|
|
294
|
+
path += ` C ${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${anchor.x},${anchor.y}`;
|
|
295
|
+
}
|
|
296
|
+
return path;
|
|
297
|
+
};
|
|
298
|
+
const buildEdgePath = (section, routing) => {
|
|
299
|
+
if (routing === "SPLINES") return buildSplinePath$1(section);
|
|
300
|
+
const start = section.startPoint;
|
|
301
|
+
const end = section.endPoint;
|
|
302
|
+
if (!start || !end) return null;
|
|
303
|
+
return buildPolylinePath$1([
|
|
304
|
+
start,
|
|
305
|
+
...section.bendPoints ?? [],
|
|
306
|
+
end
|
|
307
|
+
]);
|
|
308
|
+
};
|
|
309
|
+
const sectionMidpoint = (section) => {
|
|
310
|
+
if (!section.startPoint || !section.endPoint) return null;
|
|
311
|
+
const points = [
|
|
312
|
+
section.startPoint,
|
|
313
|
+
...section.bendPoints ?? [],
|
|
314
|
+
section.endPoint
|
|
315
|
+
];
|
|
316
|
+
const mid = points[Math.floor(points.length / 2)];
|
|
317
|
+
return mid ? {
|
|
318
|
+
x: mid.x,
|
|
319
|
+
y: mid.y
|
|
320
|
+
} : null;
|
|
321
|
+
};
|
|
322
|
+
const getEdgeLabelPosition = (label, section) => {
|
|
323
|
+
if (label?.x != null && label?.y != null) return {
|
|
324
|
+
x: label.x + (label.width ?? 0) / 2,
|
|
325
|
+
y: label.y + (label.height ?? 0) / 2
|
|
326
|
+
};
|
|
327
|
+
return sectionMidpoint(section);
|
|
328
|
+
};
|
|
329
|
+
const elk = new ELK();
|
|
330
|
+
const getEdgeRouting = (edge, fallback) => {
|
|
331
|
+
const options = edge.layoutOptions ?? {};
|
|
332
|
+
const edgeRouting = options["elk.edgeRouting"] ?? options["org.eclipse.elk.edgeRouting"];
|
|
333
|
+
return edgeRouting === void 0 ? fallback : normalizeEdgeRouting(edgeRouting);
|
|
334
|
+
};
|
|
335
|
+
const toPositionMap = (children = []) => children.reduce((acc, child) => {
|
|
336
|
+
if (child.id && child.x != null && child.y != null) acc[child.id] = {
|
|
337
|
+
x: child.x,
|
|
338
|
+
y: child.y
|
|
339
|
+
};
|
|
340
|
+
return acc;
|
|
341
|
+
}, {});
|
|
342
|
+
const toEdgeLayoutMap = (edges = [], graphRouting) => edges.reduce((acc, edge) => {
|
|
343
|
+
const section = edge.sections?.[0];
|
|
344
|
+
if (!edge.id || !section) return acc;
|
|
345
|
+
const path = buildEdgePath(section, getEdgeRouting(edge, graphRouting));
|
|
346
|
+
if (!path) return acc;
|
|
347
|
+
acc[edge.id] = {
|
|
348
|
+
path,
|
|
349
|
+
labelPosition: getEdgeLabelPosition(edge.labels?.[0], section),
|
|
350
|
+
startPoint: section.startPoint ? {
|
|
351
|
+
x: section.startPoint.x,
|
|
352
|
+
y: section.startPoint.y
|
|
353
|
+
} : null,
|
|
354
|
+
endPoint: section.endPoint ? {
|
|
355
|
+
x: section.endPoint.x,
|
|
356
|
+
y: section.endPoint.y
|
|
357
|
+
} : null
|
|
358
|
+
};
|
|
359
|
+
return acc;
|
|
360
|
+
}, {});
|
|
361
|
+
const compareById = (left, right) => left.id.localeCompare(right.id);
|
|
362
|
+
const compareEdgesForLayout = (left, right) => left.source.localeCompare(right.source) || left.target.localeCompare(right.target) || left.id.localeCompare(right.id);
|
|
363
|
+
const runElk = async ({ nodes, edges, layoutPlan }) => {
|
|
364
|
+
const mergedLayoutOptions = nodes.some((node) => typeof node.data.rank === "number") ? {
|
|
365
|
+
"elk.partitioning.activate": true,
|
|
366
|
+
...layoutPlan.elkOptions
|
|
367
|
+
} : layoutPlan.elkOptions;
|
|
368
|
+
const layoutNodes = [...nodes].sort(compareById);
|
|
369
|
+
const layoutEdges = [...edges].sort(compareEdgesForLayout);
|
|
370
|
+
const graph = await elk.layout({
|
|
371
|
+
id: "root",
|
|
372
|
+
layoutOptions: mergedLayoutOptions,
|
|
373
|
+
children: layoutNodes.map((node) => {
|
|
374
|
+
const nodeData = node.data;
|
|
375
|
+
const rank = nodeData.rank ?? null;
|
|
376
|
+
const layoutOptions = { ...nodeData.layout_attrs ?? {} };
|
|
377
|
+
if (rank === "source") layoutOptions["elk.layered.layering.layerConstraint"] = "FIRST";
|
|
378
|
+
else if (rank === "sink") layoutOptions["elk.layered.layering.layerConstraint"] = "LAST";
|
|
379
|
+
else if (typeof rank === "number") layoutOptions["elk.partitioning.partition"] = String(rank);
|
|
380
|
+
return {
|
|
381
|
+
id: node.id,
|
|
382
|
+
width: nodeData.width,
|
|
383
|
+
height: nodeData.height,
|
|
384
|
+
layoutOptions
|
|
385
|
+
};
|
|
386
|
+
}),
|
|
387
|
+
edges: layoutEdges.map((edge) => {
|
|
388
|
+
const edgeData = edge.data;
|
|
389
|
+
const edgeLabel = edgeData?.label;
|
|
390
|
+
return {
|
|
391
|
+
id: edge.id,
|
|
392
|
+
sources: [edge.source],
|
|
393
|
+
targets: [edge.target],
|
|
394
|
+
layoutOptions: edgeData?.layout_attrs ?? {},
|
|
395
|
+
...edgeLabel ? { labels: [{
|
|
396
|
+
text: String(edgeLabel),
|
|
397
|
+
width: String(edgeLabel).length * 7 + 12,
|
|
398
|
+
height: 18
|
|
399
|
+
}] } : {}
|
|
400
|
+
};
|
|
401
|
+
})
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
positions: toPositionMap(graph.children),
|
|
405
|
+
edgeLayouts: toEdgeLayoutMap(graph.edges, layoutPlan.edgeRouting)
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
const useElkLayout = () => {
|
|
409
|
+
const measuredNodes = useNodes();
|
|
410
|
+
const nodesInitialized = useNodesInitialized();
|
|
411
|
+
return useCallback(async (model) => {
|
|
412
|
+
if (!nodesInitialized || !measuredNodesMatchModel(measuredNodes, model)) return null;
|
|
413
|
+
const layout = await runElk({
|
|
414
|
+
nodes: measuredNodes,
|
|
415
|
+
edges: model.edges,
|
|
416
|
+
layoutPlan: model.layoutPlan
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
nodes: applyNodePositions(measuredNodes, layout.positions),
|
|
420
|
+
edges: applyEdgeLayouts(model.edges, layout.edgeLayouts)
|
|
421
|
+
};
|
|
422
|
+
}, [nodesInitialized, measuredNodes]);
|
|
423
|
+
};
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/useGraphvizLayout.ts
|
|
426
|
+
const GRAPHVIZ_DPI = 72;
|
|
427
|
+
const quoteDotValue = (value) => `"${value.replaceAll("\"", "\\\"")}"`;
|
|
428
|
+
const attrsToDot = (attrs) => {
|
|
429
|
+
if (!attrs) return "";
|
|
430
|
+
return Object.entries(attrs).filter(([, value]) => value != null).map(([key, value]) => typeof value === "string" ? `${key}=${quoteDotValue(value)}` : `${key}=${String(value)}`).join(",");
|
|
431
|
+
};
|
|
432
|
+
const parsePoint = (value) => {
|
|
433
|
+
if (!value) return null;
|
|
434
|
+
const [xRaw, yRaw] = value.split(",");
|
|
435
|
+
const x = Number.parseFloat(xRaw ?? "");
|
|
436
|
+
const y = Number.parseFloat(yRaw ?? "");
|
|
437
|
+
return Number.isFinite(x) && Number.isFinite(y) ? {
|
|
438
|
+
x,
|
|
439
|
+
y
|
|
440
|
+
} : null;
|
|
441
|
+
};
|
|
442
|
+
const parseBoundingBoxHeight = (bb) => {
|
|
443
|
+
const height = (bb?.split(",").map((value) => Number.parseFloat(value)))?.[3];
|
|
444
|
+
return Number.isFinite(height) ? height : 0;
|
|
445
|
+
};
|
|
446
|
+
const parseGraphvizDimension = (value) => {
|
|
447
|
+
if (value == null) return null;
|
|
448
|
+
const dimension = Number.parseFloat(String(value)) * GRAPHVIZ_DPI;
|
|
449
|
+
return Number.isFinite(dimension) ? dimension : null;
|
|
450
|
+
};
|
|
451
|
+
const parseGraphvizEdgePosition = (pos) => {
|
|
452
|
+
const result = {
|
|
453
|
+
points: [],
|
|
454
|
+
startArrowPoint: null,
|
|
455
|
+
endArrowPoint: null
|
|
456
|
+
};
|
|
457
|
+
if (!pos) return result;
|
|
458
|
+
const pointMatches = pos.matchAll(/(?:^|\s)(?:(s|e),)?(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g);
|
|
459
|
+
for (const match of pointMatches) {
|
|
460
|
+
const point = {
|
|
461
|
+
x: Number.parseFloat(match[2] ?? "0"),
|
|
462
|
+
y: Number.parseFloat(match[3] ?? "0")
|
|
463
|
+
};
|
|
464
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) continue;
|
|
465
|
+
if (match[1] === "s") result.startArrowPoint = point;
|
|
466
|
+
else if (match[1] === "e") result.endArrowPoint = point;
|
|
467
|
+
else result.points.push(point);
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
};
|
|
471
|
+
const flipY = (point, graphHeight) => ({
|
|
472
|
+
x: point.x,
|
|
473
|
+
y: graphHeight - point.y
|
|
474
|
+
});
|
|
475
|
+
const buildPolylinePath = (points) => {
|
|
476
|
+
if (points.length < 2) return null;
|
|
477
|
+
return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x},${point.y}`).join(" ");
|
|
478
|
+
};
|
|
479
|
+
const buildSplinePath = (points) => {
|
|
480
|
+
if (points.length < 4 || (points.length - 1) % 3 !== 0) return null;
|
|
481
|
+
let path = `M ${points[0]?.x},${points[0]?.y}`;
|
|
482
|
+
for (let index = 1; index < points.length; index += 3) {
|
|
483
|
+
const cp1 = points[index];
|
|
484
|
+
const cp2 = points[index + 1];
|
|
485
|
+
const end = points[index + 2];
|
|
486
|
+
if (!cp1 || !cp2 || !end) return null;
|
|
487
|
+
path += ` C ${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${end.x},${end.y}`;
|
|
488
|
+
}
|
|
489
|
+
return path;
|
|
490
|
+
};
|
|
491
|
+
const buildGraphvizEdgePath = ({ points, startArrowPoint, endArrowPoint }) => {
|
|
492
|
+
const splinePath = buildSplinePath(points);
|
|
493
|
+
if (splinePath) return [
|
|
494
|
+
startArrowPoint && points[0] ? `M ${startArrowPoint.x},${startArrowPoint.y} L ${points[0].x},${points[0].y}` : null,
|
|
495
|
+
startArrowPoint ? splinePath.replace(/^M [^C]+/, "") : splinePath,
|
|
496
|
+
endArrowPoint ? `L ${endArrowPoint.x},${endArrowPoint.y}` : null
|
|
497
|
+
].filter(Boolean).join(" ");
|
|
498
|
+
return buildPolylinePath([
|
|
499
|
+
...startArrowPoint ? [startArrowPoint] : [],
|
|
500
|
+
...points,
|
|
501
|
+
...endArrowPoint ? [endArrowPoint] : []
|
|
502
|
+
]);
|
|
503
|
+
};
|
|
504
|
+
const graphvizShape = (shape) => {
|
|
505
|
+
switch (shape) {
|
|
506
|
+
case "circle": return "circle";
|
|
507
|
+
case "diamond": return "diamond";
|
|
508
|
+
case "hexagon": return "hexagon";
|
|
509
|
+
case "triangle": return "triangle";
|
|
510
|
+
default: return "box";
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const toDot = (visualization, layoutPlan) => {
|
|
514
|
+
const lines = [
|
|
515
|
+
"digraph G {",
|
|
516
|
+
`graph [${attrsToDot(layoutPlan.graphAttrs)}];`,
|
|
517
|
+
`node [${attrsToDot(layoutPlan.nodeAttrs)}];`,
|
|
518
|
+
`edge [${attrsToDot(layoutPlan.edgeAttrs)}];`
|
|
519
|
+
];
|
|
520
|
+
for (const node of visualization.nodes ?? []) {
|
|
521
|
+
const attrs = {
|
|
522
|
+
...node.layout_attrs,
|
|
523
|
+
label: node.label ?? "",
|
|
524
|
+
shape: graphvizShape(node.shape),
|
|
525
|
+
color: node.border_color ?? node.color ?? void 0,
|
|
526
|
+
fillcolor: node.color ?? void 0,
|
|
527
|
+
style: node.color ? "filled" : void 0,
|
|
528
|
+
width: node.width ? (node.width / GRAPHVIZ_DPI).toFixed(4) : void 0,
|
|
529
|
+
height: node.height ? (node.height / GRAPHVIZ_DPI).toFixed(4) : void 0,
|
|
530
|
+
fixedsize: node.width && node.height ? true : void 0
|
|
531
|
+
};
|
|
532
|
+
lines.push(`${quoteDotValue(node.id)} [${attrsToDot(attrs)}];`);
|
|
533
|
+
}
|
|
534
|
+
for (const edge of visualization.edges ?? []) {
|
|
535
|
+
const attrs = {
|
|
536
|
+
...edge.layout_attrs,
|
|
537
|
+
id: edge.id,
|
|
538
|
+
label: edge.label ?? void 0,
|
|
539
|
+
color: edge.color ?? void 0
|
|
540
|
+
};
|
|
541
|
+
lines.push(`${quoteDotValue(edge.source)} -> ${quoteDotValue(edge.target)} [${attrsToDot(attrs)}];`);
|
|
542
|
+
}
|
|
543
|
+
const ranks = (visualization.nodes ?? []).filter(({ rank }) => rank != null).reduce((acc, node) => {
|
|
544
|
+
const key = node.rank;
|
|
545
|
+
acc[key] ??= [];
|
|
546
|
+
acc[key].push(quoteDotValue(node.id));
|
|
547
|
+
return acc;
|
|
548
|
+
}, {});
|
|
549
|
+
lines.push(...Object.entries(ranks).map(([rank, ids]) => `{ rank=${["source", "sink"].includes(rank) ? rank : "same"}; ${(ids ?? []).join(" ")} }`));
|
|
550
|
+
lines.push("}");
|
|
551
|
+
return lines.join("\n");
|
|
552
|
+
};
|
|
553
|
+
const getGraphvizEdgeKey = (edge, gvidToName) => {
|
|
554
|
+
const tail = typeof edge.tail === "number" ? gvidToName.get(edge.tail) : edge.tail;
|
|
555
|
+
const head = typeof edge.head === "number" ? gvidToName.get(edge.head) : edge.head;
|
|
556
|
+
return tail && head ? `${tail}->${head}` : null;
|
|
557
|
+
};
|
|
558
|
+
const buildGraphvizEdgeLayouts = (graphvizEdges, graphHeight, edges, gvidToName) => {
|
|
559
|
+
const fallbackCounts = /* @__PURE__ */ new Map();
|
|
560
|
+
const fallbackIds = edges.reduce((acc, edge) => {
|
|
561
|
+
const key = `${edge.source}->${edge.target}`;
|
|
562
|
+
acc[key] ??= [];
|
|
563
|
+
acc[key].push(edge.id);
|
|
564
|
+
return acc;
|
|
565
|
+
}, {});
|
|
566
|
+
return graphvizEdges.reduce((acc, edge) => {
|
|
567
|
+
const edgeKey = getGraphvizEdgeKey(edge, gvidToName);
|
|
568
|
+
const fallbackIdsForKey = edgeKey ? fallbackIds[edgeKey] : void 0;
|
|
569
|
+
const fallbackIndex = edgeKey ? fallbackCounts.get(edgeKey) ?? 0 : 0;
|
|
570
|
+
const edgeId = edge.id ?? fallbackIdsForKey?.[fallbackIndex];
|
|
571
|
+
if (!edgeId) return acc;
|
|
572
|
+
if (edgeKey) fallbackCounts.set(edgeKey, fallbackIndex + 1);
|
|
573
|
+
const position = parseGraphvizEdgePosition(edge.pos);
|
|
574
|
+
const points = position.points.map((point) => flipY(point, graphHeight));
|
|
575
|
+
const startArrowPoint = position.startArrowPoint ? flipY(position.startArrowPoint, graphHeight) : null;
|
|
576
|
+
const endArrowPoint = position.endArrowPoint ? flipY(position.endArrowPoint, graphHeight) : null;
|
|
577
|
+
const path = buildGraphvizEdgePath({
|
|
578
|
+
points,
|
|
579
|
+
startArrowPoint,
|
|
580
|
+
endArrowPoint
|
|
581
|
+
});
|
|
582
|
+
if (!path) return acc;
|
|
583
|
+
const labelPosition = parsePoint(edge.lp);
|
|
584
|
+
acc[edgeId] = {
|
|
585
|
+
path,
|
|
586
|
+
labelPosition: labelPosition ? flipY(labelPosition, graphHeight) : null,
|
|
587
|
+
startPoint: startArrowPoint ?? points[0] ?? null,
|
|
588
|
+
endPoint: endArrowPoint ?? points.at(-1) ?? null
|
|
589
|
+
};
|
|
590
|
+
return acc;
|
|
591
|
+
}, {});
|
|
592
|
+
};
|
|
593
|
+
const runGraphviz = async ({ visualization, nodes, edges, layoutPlan }) => {
|
|
594
|
+
const graphviz = await Graphviz.load();
|
|
595
|
+
const json = JSON.parse(graphviz.layout(toDot(visualization, layoutPlan), "json", layoutPlan.engine));
|
|
596
|
+
const graphHeight = parseBoundingBoxHeight(json.bb);
|
|
597
|
+
const gvidToName = /* @__PURE__ */ new Map();
|
|
598
|
+
return {
|
|
599
|
+
positions: (json.objects ?? []).reduce((acc, object) => {
|
|
600
|
+
if (object._gvid != null && object.name) gvidToName.set(object._gvid, object.name);
|
|
601
|
+
if (!object.name) return acc;
|
|
602
|
+
const center = parsePoint(object.pos);
|
|
603
|
+
if (!center) return acc;
|
|
604
|
+
const data = nodes.find((candidate) => candidate.id === object.name)?.data;
|
|
605
|
+
const width = data?.width ?? parseGraphvizDimension(object.width) ?? 0;
|
|
606
|
+
const height = data?.height ?? parseGraphvizDimension(object.height) ?? 0;
|
|
607
|
+
const flipped = flipY(center, graphHeight);
|
|
608
|
+
acc[object.name] = {
|
|
609
|
+
x: flipped.x - width / 2,
|
|
610
|
+
y: flipped.y - height / 2
|
|
611
|
+
};
|
|
612
|
+
return acc;
|
|
613
|
+
}, {}),
|
|
614
|
+
edgeLayouts: buildGraphvizEdgeLayouts(json.edges ?? [], graphHeight, edges, gvidToName)
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
const useGraphvizLayout = (visualization) => useCallback(async (model) => {
|
|
618
|
+
const layout = await runGraphviz({
|
|
619
|
+
visualization,
|
|
620
|
+
nodes: model.nodes,
|
|
621
|
+
edges: model.edges,
|
|
622
|
+
layoutPlan: model.layoutPlan
|
|
623
|
+
});
|
|
624
|
+
return {
|
|
625
|
+
nodes: applyNodePositions(model.nodes, layout.positions),
|
|
626
|
+
edges: applyEdgeLayouts(model.edges, layout.edgeLayouts)
|
|
627
|
+
};
|
|
628
|
+
}, [visualization]);
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/useLayout.ts
|
|
631
|
+
const buildSafely = (visualization) => {
|
|
632
|
+
try {
|
|
633
|
+
return {
|
|
634
|
+
model: buildGraphFlowModel(visualization),
|
|
635
|
+
buildError: null
|
|
636
|
+
};
|
|
637
|
+
} catch (err) {
|
|
638
|
+
return {
|
|
639
|
+
model: null,
|
|
640
|
+
buildError: toGraphError(err)
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
const isElkModel = (model) => model.layoutPlan.type === "elk";
|
|
645
|
+
const isGraphvizModel = (model) => model.layoutPlan.type === "graphviz";
|
|
646
|
+
const useLayout = (visualization) => {
|
|
647
|
+
const { model, buildError } = useMemo(() => buildSafely(visualization), [visualization]);
|
|
648
|
+
const runElkLayout = useElkLayout();
|
|
649
|
+
const runGraphvizLayout = useGraphvizLayout(visualization);
|
|
650
|
+
const [nodes, setNodes] = useState([]);
|
|
651
|
+
const [edges, setEdges] = useState([]);
|
|
652
|
+
const [layoutReady, setLayoutReady] = useState(false);
|
|
653
|
+
const [layoutError, setLayoutError] = useState(null);
|
|
654
|
+
useEffect(() => {
|
|
655
|
+
setLayoutError(null);
|
|
656
|
+
setNodes(model?.nodes ?? []);
|
|
657
|
+
setEdges(model?.edges ?? []);
|
|
658
|
+
setLayoutReady(!model || model.nodes.length === 0);
|
|
659
|
+
}, [model]);
|
|
660
|
+
useEffect(() => {
|
|
661
|
+
if (!model || buildError || layoutReady) return;
|
|
662
|
+
let cancelled = false;
|
|
663
|
+
(isElkModel(model) ? runElkLayout(model) : isGraphvizModel(model) ? runGraphvizLayout(model) : Promise.resolve(null)).then((result) => {
|
|
664
|
+
if (cancelled || !result) return;
|
|
665
|
+
setNodes(result.nodes);
|
|
666
|
+
setEdges(result.edges);
|
|
667
|
+
setLayoutReady(true);
|
|
668
|
+
}, (err) => {
|
|
669
|
+
if (cancelled) return;
|
|
670
|
+
setLayoutError(toGraphError(err));
|
|
671
|
+
setLayoutReady(true);
|
|
672
|
+
});
|
|
673
|
+
return () => {
|
|
674
|
+
cancelled = true;
|
|
675
|
+
};
|
|
676
|
+
}, [
|
|
677
|
+
model,
|
|
678
|
+
buildError,
|
|
679
|
+
layoutReady,
|
|
680
|
+
runElkLayout,
|
|
681
|
+
runGraphvizLayout
|
|
682
|
+
]);
|
|
683
|
+
const onNodesChange = useCallback((changes) => setNodes((current) => applyNodeChanges(changes, current)), []);
|
|
684
|
+
return {
|
|
685
|
+
nodes,
|
|
686
|
+
edges,
|
|
687
|
+
layoutReady,
|
|
688
|
+
error: buildError ?? layoutError,
|
|
689
|
+
onNodesChange
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/Node.tsx
|
|
694
|
+
const HIDDEN_HANDLE_STYLE = {
|
|
695
|
+
opacity: 0,
|
|
696
|
+
pointerEvents: "none"
|
|
697
|
+
};
|
|
698
|
+
const HiddenHandles = () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Handle, {
|
|
699
|
+
type: "target",
|
|
700
|
+
position: Position.Left,
|
|
701
|
+
style: HIDDEN_HANDLE_STYLE
|
|
702
|
+
}), /* @__PURE__ */ jsx(Handle, {
|
|
703
|
+
type: "source",
|
|
704
|
+
position: Position.Right,
|
|
705
|
+
style: HIDDEN_HANDLE_STYLE
|
|
706
|
+
})] });
|
|
707
|
+
const AnnotationBadge = () => /* @__PURE__ */ jsx(Badge, {
|
|
708
|
+
size: "xs",
|
|
709
|
+
variant: "filled",
|
|
710
|
+
color: "gray",
|
|
711
|
+
title: "This graph element has an annotation visualization.",
|
|
712
|
+
children: "i"
|
|
713
|
+
});
|
|
714
|
+
const NodeAnnotation = ({ annotation }) => {
|
|
715
|
+
const [opened, setOpened] = useState(false);
|
|
716
|
+
const toggle = useCallback((e) => {
|
|
717
|
+
e.stopPropagation();
|
|
718
|
+
setOpened((o) => !o);
|
|
719
|
+
}, []);
|
|
720
|
+
return /* @__PURE__ */ jsxs(Popover, {
|
|
721
|
+
opened,
|
|
722
|
+
onClose: () => setOpened(false),
|
|
723
|
+
position: "right",
|
|
724
|
+
withArrow: true,
|
|
725
|
+
withinPortal: true,
|
|
726
|
+
shadow: "md",
|
|
727
|
+
children: [/* @__PURE__ */ jsx(Popover.Target, { children: /* @__PURE__ */ jsx("button", {
|
|
728
|
+
type: "button",
|
|
729
|
+
className: "nodrag nopan",
|
|
730
|
+
style: {
|
|
731
|
+
position: "absolute",
|
|
732
|
+
right: -8,
|
|
733
|
+
top: -8,
|
|
734
|
+
cursor: "pointer",
|
|
735
|
+
pointerEvents: "all",
|
|
736
|
+
zIndex: 10,
|
|
737
|
+
background: "none",
|
|
738
|
+
border: "none",
|
|
739
|
+
padding: 0
|
|
740
|
+
},
|
|
741
|
+
onClick: toggle,
|
|
742
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
743
|
+
children: /* @__PURE__ */ jsx(AnnotationBadge, {})
|
|
744
|
+
}) }), /* @__PURE__ */ jsxs(Popover.Dropdown, {
|
|
745
|
+
style: {
|
|
746
|
+
minWidth: 200,
|
|
747
|
+
maxWidth: 400
|
|
748
|
+
},
|
|
749
|
+
children: [/* @__PURE__ */ jsx(Group, {
|
|
750
|
+
justify: "flex-end",
|
|
751
|
+
mb: 4,
|
|
752
|
+
children: /* @__PURE__ */ jsx(ActionIcon, {
|
|
753
|
+
size: "xs",
|
|
754
|
+
variant: "subtle",
|
|
755
|
+
color: "gray",
|
|
756
|
+
onClick: () => setOpened(false),
|
|
757
|
+
children: /* @__PURE__ */ jsx(XIcon, { size: 12 })
|
|
758
|
+
})
|
|
759
|
+
}), /* @__PURE__ */ jsx(ScrollArea.Autosize, {
|
|
760
|
+
mah: 300,
|
|
761
|
+
children: /* @__PURE__ */ jsx(Visualization, { visualization: annotation })
|
|
762
|
+
})]
|
|
763
|
+
})]
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
const ExternalLabel = ({ children, placement }) => /* @__PURE__ */ jsx(Text, {
|
|
767
|
+
size: "xs",
|
|
768
|
+
fw: 600,
|
|
769
|
+
style: {
|
|
770
|
+
position: "absolute",
|
|
771
|
+
[placement === "top" ? "bottom" : "top"]: "100%",
|
|
772
|
+
left: "50%",
|
|
773
|
+
transform: `translate(-50%, ${placement === "top" ? "-4px" : "4px"})`,
|
|
774
|
+
whiteSpace: "nowrap",
|
|
775
|
+
color: DEFAULT_COLORS.text,
|
|
776
|
+
pointerEvents: "none",
|
|
777
|
+
letterSpacing: "0.01em"
|
|
778
|
+
},
|
|
779
|
+
children
|
|
780
|
+
});
|
|
781
|
+
const InnerSymbol = ({ symbol, color }) => {
|
|
782
|
+
if (symbol === "triangle") return /* @__PURE__ */ jsx("div", { style: {
|
|
783
|
+
width: 0,
|
|
784
|
+
height: 0,
|
|
785
|
+
borderTop: `${12 * .6}px solid transparent`,
|
|
786
|
+
borderBottom: `${12 * .6}px solid transparent`,
|
|
787
|
+
borderLeft: `12px solid ${color}`,
|
|
788
|
+
marginLeft: 2
|
|
789
|
+
} });
|
|
790
|
+
return /* @__PURE__ */ jsx("div", { style: {
|
|
791
|
+
width: 12,
|
|
792
|
+
height: 12,
|
|
793
|
+
backgroundColor: color
|
|
794
|
+
} });
|
|
795
|
+
};
|
|
796
|
+
const CLIP_PATH = {
|
|
797
|
+
triangle: "polygon(50% 0, 100% 100%, 0 100%)",
|
|
798
|
+
diamond: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)",
|
|
799
|
+
hexagon: "polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%)"
|
|
800
|
+
};
|
|
801
|
+
const CircleShape = ({ width, height, color, borderColor, doubleBorder, innerSymbol, initialTokens, finalTokens, centerLabel }) => /* @__PURE__ */ jsxs("div", {
|
|
802
|
+
style: {
|
|
803
|
+
position: "relative",
|
|
804
|
+
boxSizing: "border-box",
|
|
805
|
+
width,
|
|
806
|
+
height,
|
|
807
|
+
borderRadius: "50%",
|
|
808
|
+
backgroundColor: color,
|
|
809
|
+
border: borderColor ? `2px solid ${borderColor}` : "none",
|
|
810
|
+
display: "flex",
|
|
811
|
+
flexDirection: "column",
|
|
812
|
+
alignItems: "center",
|
|
813
|
+
justifyContent: "center",
|
|
814
|
+
gap: 2
|
|
815
|
+
},
|
|
816
|
+
children: [
|
|
817
|
+
doubleBorder && /* @__PURE__ */ jsx("div", { style: {
|
|
818
|
+
position: "absolute",
|
|
819
|
+
inset: 5,
|
|
820
|
+
borderRadius: "50%",
|
|
821
|
+
border: borderColor ? `2px solid ${borderColor}` : "none",
|
|
822
|
+
pointerEvents: "none"
|
|
823
|
+
} }),
|
|
824
|
+
initialTokens && !innerSymbol && (initialTokens === 1 ? /* @__PURE__ */ jsx("div", { style: {
|
|
825
|
+
width: 12,
|
|
826
|
+
height: 12,
|
|
827
|
+
borderRadius: "50%",
|
|
828
|
+
backgroundColor: "rgba(0,0,0,0.65)"
|
|
829
|
+
} }) : /* @__PURE__ */ jsx("span", {
|
|
551
830
|
style: {
|
|
831
|
+
color: "#111",
|
|
832
|
+
fontSize: 11,
|
|
833
|
+
fontWeight: 700,
|
|
834
|
+
lineHeight: 1
|
|
835
|
+
},
|
|
836
|
+
children: initialTokens
|
|
837
|
+
})),
|
|
838
|
+
finalTokens && finalTokens > 1 && /* @__PURE__ */ jsx("span", {
|
|
839
|
+
style: {
|
|
840
|
+
position: "absolute",
|
|
841
|
+
right: 4,
|
|
842
|
+
bottom: 3,
|
|
843
|
+
color: "#111",
|
|
844
|
+
fontSize: 9,
|
|
845
|
+
fontWeight: 700,
|
|
846
|
+
lineHeight: 1
|
|
847
|
+
},
|
|
848
|
+
children: finalTokens
|
|
849
|
+
}),
|
|
850
|
+
innerSymbol && /* @__PURE__ */ jsx(InnerSymbol, {
|
|
851
|
+
symbol: innerSymbol,
|
|
852
|
+
color: borderColor ?? "#111"
|
|
853
|
+
}),
|
|
854
|
+
centerLabel && /* @__PURE__ */ jsx("span", {
|
|
855
|
+
style: {
|
|
856
|
+
maxWidth: width - 6,
|
|
857
|
+
color: "#111",
|
|
858
|
+
fontSize: 10,
|
|
859
|
+
fontWeight: 600,
|
|
860
|
+
lineHeight: 1,
|
|
861
|
+
overflow: "hidden",
|
|
862
|
+
textAlign: "center",
|
|
863
|
+
textOverflow: "ellipsis",
|
|
864
|
+
whiteSpace: "nowrap"
|
|
865
|
+
},
|
|
866
|
+
children: centerLabel
|
|
867
|
+
})
|
|
868
|
+
]
|
|
869
|
+
});
|
|
870
|
+
const BoxShape = ({ shape, color, borderColor, doubleBorder, innerSymbol, width, height, children }) => {
|
|
871
|
+
const clipPath = CLIP_PATH[shape];
|
|
872
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
873
|
+
style: {
|
|
874
|
+
position: "relative",
|
|
875
|
+
boxSizing: "border-box",
|
|
876
|
+
width: width ?? void 0,
|
|
877
|
+
height: height ?? void 0,
|
|
878
|
+
padding: borderColor != null ? 2 : 0,
|
|
879
|
+
backgroundColor: borderColor ?? "transparent",
|
|
880
|
+
borderRadius: shape === "rectangle" ? 5 : 0,
|
|
881
|
+
clipPath
|
|
882
|
+
},
|
|
883
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
884
|
+
style: {
|
|
885
|
+
boxSizing: "border-box",
|
|
552
886
|
width: "100%",
|
|
553
887
|
height: "100%",
|
|
554
|
-
|
|
888
|
+
backgroundColor: color,
|
|
889
|
+
borderRadius: shape === "rectangle" ? 3 : 0,
|
|
890
|
+
clipPath,
|
|
891
|
+
display: "flex",
|
|
892
|
+
alignItems: "center",
|
|
893
|
+
justifyContent: "center",
|
|
894
|
+
overflow: "hidden"
|
|
555
895
|
},
|
|
556
|
-
children:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
896
|
+
children: innerSymbol && !children ? /* @__PURE__ */ jsx(InnerSymbol, {
|
|
897
|
+
symbol: innerSymbol,
|
|
898
|
+
color: "#111"
|
|
899
|
+
}) : children
|
|
900
|
+
}), doubleBorder && /* @__PURE__ */ jsx("div", { style: {
|
|
901
|
+
position: "absolute",
|
|
902
|
+
inset: 6,
|
|
903
|
+
border: borderColor ? `1.5px solid ${borderColor}` : "none",
|
|
904
|
+
borderRadius: shape === "rectangle" ? 3 : 0,
|
|
905
|
+
clipPath,
|
|
906
|
+
pointerEvents: "none"
|
|
907
|
+
} })]
|
|
908
|
+
});
|
|
909
|
+
};
|
|
910
|
+
const GraphFlowNode = memo(({ data }) => {
|
|
911
|
+
const { shape, label, color, border_color, annotation, label_pos, width, height, style } = data;
|
|
912
|
+
const doubleBorder = style?.double_border ?? null;
|
|
913
|
+
const innerSymbol = style?.inner_symbol ?? null;
|
|
914
|
+
const initialTokens = style?.initial_tokens ?? null;
|
|
915
|
+
const finalTokens = style?.final_tokens ?? null;
|
|
916
|
+
const isExternalLabel = Boolean(label && label_pos !== "center" && label_pos != null);
|
|
917
|
+
const maxLabelLines = height != null ? Math.max(1, Math.floor((height - 4) / (14 * 1.2))) : void 0;
|
|
918
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
919
|
+
style: {
|
|
920
|
+
position: "relative",
|
|
921
|
+
width: shape === "circle" ? width ?? void 0 : void 0
|
|
922
|
+
},
|
|
923
|
+
children: [
|
|
924
|
+
/* @__PURE__ */ jsx(HiddenHandles, {}),
|
|
925
|
+
isExternalLabel && label_pos === "top" && /* @__PURE__ */ jsx(ExternalLabel, {
|
|
926
|
+
placement: "top",
|
|
927
|
+
children: label
|
|
928
|
+
}),
|
|
929
|
+
shape === "circle" ? /* @__PURE__ */ jsx(CircleShape, {
|
|
930
|
+
width,
|
|
931
|
+
height,
|
|
932
|
+
color: color ?? DEFAULT_COLORS.transition,
|
|
933
|
+
borderColor: border_color ?? null,
|
|
934
|
+
doubleBorder,
|
|
935
|
+
innerSymbol,
|
|
936
|
+
initialTokens,
|
|
937
|
+
finalTokens,
|
|
938
|
+
centerLabel: label_pos === "center" ? label ?? null : null
|
|
939
|
+
}) : /* @__PURE__ */ jsx(BoxShape, {
|
|
940
|
+
shape,
|
|
941
|
+
color: color ?? DEFAULT_COLORS.transition,
|
|
942
|
+
borderColor: border_color ?? null,
|
|
943
|
+
doubleBorder,
|
|
944
|
+
innerSymbol,
|
|
945
|
+
width: width ?? null,
|
|
946
|
+
height: height ?? null,
|
|
947
|
+
children: !isExternalLabel && label && /* @__PURE__ */ jsx(Text, {
|
|
948
|
+
size: "sm",
|
|
949
|
+
fw: 500,
|
|
950
|
+
ta: "center",
|
|
951
|
+
px: 12,
|
|
952
|
+
style: {
|
|
953
|
+
lineHeight: 1.2,
|
|
954
|
+
color: "#111",
|
|
955
|
+
maxWidth: width != null ? width : "100%",
|
|
956
|
+
boxSizing: "border-box",
|
|
957
|
+
overflow: "hidden",
|
|
958
|
+
overflowWrap: "anywhere",
|
|
959
|
+
...maxLabelLines != null ? {
|
|
960
|
+
display: "-webkit-box",
|
|
961
|
+
WebkitBoxOrient: "vertical",
|
|
962
|
+
WebkitLineClamp: maxLabelLines
|
|
963
|
+
} : { whiteSpace: "normal" }
|
|
964
|
+
},
|
|
965
|
+
children: label
|
|
966
|
+
})
|
|
967
|
+
}),
|
|
968
|
+
isExternalLabel && label_pos === "bottom" && /* @__PURE__ */ jsx(ExternalLabel, {
|
|
969
|
+
placement: "bottom",
|
|
970
|
+
children: label
|
|
971
|
+
}),
|
|
972
|
+
annotation && /* @__PURE__ */ jsx(NodeAnnotation, { annotation })
|
|
973
|
+
]
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
GraphFlowNode.displayName = "GraphFlowNode";
|
|
977
|
+
//#endregion
|
|
978
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/Edge.tsx
|
|
979
|
+
const placeBounds = (node) => {
|
|
980
|
+
const data = node.data;
|
|
981
|
+
const width = data.width ?? 0;
|
|
982
|
+
const height = data.height ?? 0;
|
|
983
|
+
return {
|
|
984
|
+
x: node.internals.positionAbsolute.x,
|
|
985
|
+
y: node.internals.positionAbsolute.y,
|
|
986
|
+
width,
|
|
987
|
+
height
|
|
988
|
+
};
|
|
989
|
+
};
|
|
990
|
+
const visualBounds = (node) => {
|
|
991
|
+
if (node.data.shape === "circle") return placeBounds(node);
|
|
992
|
+
return {
|
|
993
|
+
x: node.internals.positionAbsolute.x,
|
|
994
|
+
y: node.internals.positionAbsolute.y,
|
|
995
|
+
width: node.measured.width ?? 0,
|
|
996
|
+
height: node.measured.height ?? 0
|
|
997
|
+
};
|
|
998
|
+
};
|
|
999
|
+
const center = (node) => {
|
|
1000
|
+
const bounds = visualBounds(node);
|
|
1001
|
+
return {
|
|
1002
|
+
x: bounds.x + bounds.width / 2,
|
|
1003
|
+
y: bounds.y + bounds.height / 2
|
|
1004
|
+
};
|
|
1005
|
+
};
|
|
1006
|
+
const rectBorderPoint = (node, other) => {
|
|
1007
|
+
const bounds = visualBounds(node);
|
|
1008
|
+
const hw = bounds.width / 2;
|
|
1009
|
+
const hh = bounds.height / 2;
|
|
1010
|
+
if (hw === 0 || hh === 0) return null;
|
|
1011
|
+
const nc = center(node);
|
|
1012
|
+
const oc = center(other);
|
|
1013
|
+
const dx = oc.x - nc.x;
|
|
1014
|
+
const dy = oc.y - nc.y;
|
|
1015
|
+
const xx = dx / (2 * hw) - dy / (2 * hh);
|
|
1016
|
+
const yy = dx / (2 * hw) + dy / (2 * hh);
|
|
1017
|
+
const scale = 1 / (Math.abs(xx) + Math.abs(yy));
|
|
1018
|
+
if (!Number.isFinite(scale)) return null;
|
|
1019
|
+
return {
|
|
1020
|
+
x: nc.x + hw * scale * (xx + yy),
|
|
1021
|
+
y: nc.y + hh * scale * (-xx + yy)
|
|
1022
|
+
};
|
|
1023
|
+
};
|
|
1024
|
+
const circleBorderPoint = (node, other) => {
|
|
1025
|
+
const bounds = placeBounds(node);
|
|
1026
|
+
const rx = bounds.width / 2;
|
|
1027
|
+
const ry = bounds.height / 2;
|
|
1028
|
+
if (rx === 0 || ry === 0) return null;
|
|
1029
|
+
const nc = center(node);
|
|
1030
|
+
const oc = center(other);
|
|
1031
|
+
const angle = Math.atan2(oc.y - nc.y, oc.x - nc.x);
|
|
1032
|
+
return {
|
|
1033
|
+
x: nc.x + Math.cos(angle) * rx,
|
|
1034
|
+
y: nc.y + Math.sin(angle) * ry
|
|
1035
|
+
};
|
|
1036
|
+
};
|
|
1037
|
+
const borderPoint = (node, other) => node.data.shape === "circle" ? circleBorderPoint(node, other) : rectBorderPoint(node, other);
|
|
1038
|
+
const edgeSide = (node, pt) => {
|
|
1039
|
+
const nc = center(node);
|
|
1040
|
+
const dx = pt.x - nc.x;
|
|
1041
|
+
const dy = pt.y - nc.y;
|
|
1042
|
+
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? Position.Right : Position.Left;
|
|
1043
|
+
return dy >= 0 ? Position.Bottom : Position.Top;
|
|
1044
|
+
};
|
|
1045
|
+
const getEdgeParams = (source, target) => {
|
|
1046
|
+
const sp = borderPoint(source, target);
|
|
1047
|
+
const tp = borderPoint(target, source);
|
|
1048
|
+
if (!sp || !tp) return null;
|
|
1049
|
+
return {
|
|
1050
|
+
sx: sp.x,
|
|
1051
|
+
sy: sp.y,
|
|
1052
|
+
tx: tp.x,
|
|
1053
|
+
ty: tp.y,
|
|
1054
|
+
sourcePos: edgeSide(source, sp),
|
|
1055
|
+
targetPos: edgeSide(target, tp)
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
const fallbackLabelPosition = ({ sourceX, sourceY, targetX, targetY }) => ({
|
|
1059
|
+
x: (sourceX + targetX) / 2,
|
|
1060
|
+
y: (sourceY + targetY) / 2
|
|
1061
|
+
});
|
|
1062
|
+
const getSelfLoopPath = (sourceNode) => {
|
|
1063
|
+
const width = sourceNode.measured.width ?? 60;
|
|
1064
|
+
const height = sourceNode.measured.height ?? 34;
|
|
1065
|
+
const position = sourceNode.internals.positionAbsolute;
|
|
1066
|
+
const [path, labelX, labelY] = getSmoothStepPath({
|
|
1067
|
+
sourceX: position.x + width,
|
|
1068
|
+
sourceY: position.y + height * .25,
|
|
1069
|
+
sourcePosition: "right",
|
|
1070
|
+
targetX: position.x + width,
|
|
1071
|
+
targetY: position.y + height * .75,
|
|
1072
|
+
targetPosition: "right",
|
|
1073
|
+
borderRadius: 20,
|
|
1074
|
+
offset: 50
|
|
1075
|
+
});
|
|
1076
|
+
return {
|
|
1077
|
+
path,
|
|
1078
|
+
labelX,
|
|
1079
|
+
labelY
|
|
1080
|
+
};
|
|
1081
|
+
};
|
|
1082
|
+
const getFloatingPath = ({ sourceNode, targetNode, fallback }) => {
|
|
1083
|
+
const params = sourceNode && targetNode ? getEdgeParams(sourceNode, targetNode) : null;
|
|
1084
|
+
const [path, labelX, labelY] = getBezierPath(params ? {
|
|
1085
|
+
sourceX: params.sx,
|
|
1086
|
+
sourceY: params.sy,
|
|
1087
|
+
sourcePosition: params.sourcePos,
|
|
1088
|
+
targetX: params.tx,
|
|
1089
|
+
targetY: params.ty,
|
|
1090
|
+
targetPosition: params.targetPos
|
|
1091
|
+
} : fallback);
|
|
1092
|
+
return {
|
|
1093
|
+
path,
|
|
1094
|
+
labelX,
|
|
1095
|
+
labelY
|
|
1096
|
+
};
|
|
1097
|
+
};
|
|
1098
|
+
const resolveEdgePath = ({ edge, sourceNode, targetNode }) => {
|
|
1099
|
+
const { data, source, target, sourceX, sourceY, targetX, targetY } = edge;
|
|
1100
|
+
const customLabelPosition = fallbackLabelPosition(edge);
|
|
1101
|
+
if (data?.path) return {
|
|
1102
|
+
path: data.path,
|
|
1103
|
+
labelX: data.labelPosition?.x ?? customLabelPosition.x,
|
|
1104
|
+
labelY: data.labelPosition?.y ?? customLabelPosition.y
|
|
1105
|
+
};
|
|
1106
|
+
if (source === target && sourceNode) return getSelfLoopPath(sourceNode);
|
|
1107
|
+
return getFloatingPath({
|
|
1108
|
+
sourceNode,
|
|
1109
|
+
targetNode,
|
|
1110
|
+
fallback: {
|
|
1111
|
+
sourceX,
|
|
1112
|
+
sourceY,
|
|
1113
|
+
sourcePosition: edge.sourcePosition,
|
|
1114
|
+
targetX,
|
|
1115
|
+
targetY,
|
|
1116
|
+
targetPosition: edge.targetPosition
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
};
|
|
1120
|
+
const colorToId = (color) => color.replace(/[^a-zA-Z0-9]/g, "");
|
|
1121
|
+
const ArrowMarker = ({ id, type, color, isStart }) => {
|
|
1122
|
+
if (!type) return null;
|
|
1123
|
+
const commonProps = {
|
|
1124
|
+
id,
|
|
1125
|
+
orient: isStart ? "auto-start-reverse" : "auto",
|
|
1126
|
+
markerUnits: "strokeWidth"
|
|
1127
|
+
};
|
|
1128
|
+
switch (type) {
|
|
1129
|
+
case "triangle": return /* @__PURE__ */ jsx("marker", {
|
|
1130
|
+
...commonProps,
|
|
1131
|
+
markerWidth: "8",
|
|
1132
|
+
markerHeight: "6",
|
|
1133
|
+
refX: "7",
|
|
1134
|
+
refY: "3",
|
|
1135
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1136
|
+
d: "M0,0 L0,6 L8,3 z",
|
|
1137
|
+
fill: color
|
|
1138
|
+
})
|
|
1139
|
+
});
|
|
1140
|
+
case "vee": return /* @__PURE__ */ jsx("marker", {
|
|
1141
|
+
...commonProps,
|
|
1142
|
+
markerWidth: "8",
|
|
1143
|
+
markerHeight: "6",
|
|
1144
|
+
refX: "6",
|
|
1145
|
+
refY: "3",
|
|
1146
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1147
|
+
d: "M0,0 L8,3 L0,6",
|
|
1148
|
+
fill: "none",
|
|
1149
|
+
stroke: color,
|
|
1150
|
+
strokeWidth: "1.5"
|
|
1151
|
+
})
|
|
1152
|
+
});
|
|
1153
|
+
case "chevron": return /* @__PURE__ */ jsx("marker", {
|
|
1154
|
+
...commonProps,
|
|
1155
|
+
markerWidth: "6",
|
|
1156
|
+
markerHeight: "6",
|
|
1157
|
+
refX: "5",
|
|
1158
|
+
refY: "3",
|
|
1159
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1160
|
+
d: "M0,0 L5,3 L0,6",
|
|
1161
|
+
fill: "none",
|
|
1162
|
+
stroke: color,
|
|
1163
|
+
strokeWidth: "1.5"
|
|
1164
|
+
})
|
|
1165
|
+
});
|
|
1166
|
+
case "tee": return /* @__PURE__ */ jsx("marker", {
|
|
1167
|
+
...commonProps,
|
|
1168
|
+
markerWidth: "3",
|
|
1169
|
+
markerHeight: "6",
|
|
1170
|
+
refX: "1",
|
|
1171
|
+
refY: "3",
|
|
1172
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1173
|
+
d: "M1,0 L1,6",
|
|
1174
|
+
stroke: color,
|
|
1175
|
+
strokeWidth: "1.5",
|
|
1176
|
+
fill: "none"
|
|
1177
|
+
})
|
|
1178
|
+
});
|
|
1179
|
+
case "circle": return /* @__PURE__ */ jsx("marker", {
|
|
1180
|
+
...commonProps,
|
|
1181
|
+
markerWidth: "7",
|
|
1182
|
+
markerHeight: "6",
|
|
1183
|
+
refX: "6",
|
|
1184
|
+
refY: "3",
|
|
1185
|
+
children: /* @__PURE__ */ jsx("circle", {
|
|
1186
|
+
cx: "3",
|
|
1187
|
+
cy: "3",
|
|
1188
|
+
r: "2.5",
|
|
1189
|
+
fill: color
|
|
1190
|
+
})
|
|
1191
|
+
});
|
|
1192
|
+
case "circle-triangle": return /* @__PURE__ */ jsxs("marker", {
|
|
1193
|
+
...commonProps,
|
|
1194
|
+
markerWidth: "13",
|
|
1195
|
+
markerHeight: "6",
|
|
1196
|
+
refX: "12",
|
|
1197
|
+
refY: "3",
|
|
1198
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
1199
|
+
cx: "2.5",
|
|
1200
|
+
cy: "3",
|
|
1201
|
+
r: "2.5",
|
|
1202
|
+
fill: color
|
|
1203
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
1204
|
+
d: "M5,0 L5,6 L13,3 z",
|
|
1205
|
+
fill: color
|
|
581
1206
|
})]
|
|
1207
|
+
});
|
|
1208
|
+
case "triangle-backcurve": return /* @__PURE__ */ jsx("marker", {
|
|
1209
|
+
...commonProps,
|
|
1210
|
+
markerWidth: "8",
|
|
1211
|
+
markerHeight: "6",
|
|
1212
|
+
refX: "7",
|
|
1213
|
+
refY: "3",
|
|
1214
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1215
|
+
d: "M0,0 Q4,3 0,6 L8,3 z",
|
|
1216
|
+
fill: color
|
|
1217
|
+
})
|
|
1218
|
+
});
|
|
1219
|
+
case "triangle-tee": return /* @__PURE__ */ jsxs("marker", {
|
|
1220
|
+
...commonProps,
|
|
1221
|
+
markerWidth: "11",
|
|
1222
|
+
markerHeight: "6",
|
|
1223
|
+
refX: "10",
|
|
1224
|
+
refY: "3",
|
|
1225
|
+
children: [/* @__PURE__ */ jsx("path", {
|
|
1226
|
+
d: "M0,0 L0,6",
|
|
1227
|
+
stroke: color,
|
|
1228
|
+
strokeWidth: "1.5",
|
|
1229
|
+
fill: "none"
|
|
1230
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
1231
|
+
d: "M2,0 L2,6 L10,3 z",
|
|
1232
|
+
fill: color
|
|
1233
|
+
})]
|
|
1234
|
+
});
|
|
1235
|
+
case "triangle-cross": return /* @__PURE__ */ jsxs("marker", {
|
|
1236
|
+
...commonProps,
|
|
1237
|
+
markerWidth: "11",
|
|
1238
|
+
markerHeight: "6",
|
|
1239
|
+
refX: "10",
|
|
1240
|
+
refY: "3",
|
|
1241
|
+
children: [/* @__PURE__ */ jsx("path", {
|
|
1242
|
+
d: "M0,0 L4,6 M4,0 L0,6",
|
|
1243
|
+
stroke: color,
|
|
1244
|
+
strokeWidth: "1.5",
|
|
1245
|
+
fill: "none"
|
|
1246
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
1247
|
+
d: "M5,0 L5,6 L11,3 z",
|
|
1248
|
+
fill: color
|
|
1249
|
+
})]
|
|
1250
|
+
});
|
|
1251
|
+
case "square": return /* @__PURE__ */ jsx("marker", {
|
|
1252
|
+
...commonProps,
|
|
1253
|
+
markerWidth: "7",
|
|
1254
|
+
markerHeight: "6",
|
|
1255
|
+
refX: "6",
|
|
1256
|
+
refY: "3",
|
|
1257
|
+
children: /* @__PURE__ */ jsx("rect", {
|
|
1258
|
+
x: "0",
|
|
1259
|
+
y: "0",
|
|
1260
|
+
width: "6",
|
|
1261
|
+
height: "6",
|
|
1262
|
+
fill: color
|
|
1263
|
+
})
|
|
1264
|
+
});
|
|
1265
|
+
case "diamond": return /* @__PURE__ */ jsx("marker", {
|
|
1266
|
+
...commonProps,
|
|
1267
|
+
markerWidth: "9",
|
|
1268
|
+
markerHeight: "6",
|
|
1269
|
+
refX: "8",
|
|
1270
|
+
refY: "3",
|
|
1271
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1272
|
+
d: "M0,3 L4,0 L8,3 L4,6 z",
|
|
1273
|
+
fill: color
|
|
1274
|
+
})
|
|
1275
|
+
});
|
|
1276
|
+
default: return null;
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
const getMarkerIds = ({ color, startArrow, endArrow }) => {
|
|
1280
|
+
const colorKey = colorToId(color);
|
|
1281
|
+
return {
|
|
1282
|
+
startMarkerId: startArrow ? `arrow-start-${startArrow}-${colorKey}` : null,
|
|
1283
|
+
endMarkerId: endArrow ? `arrow-end-${endArrow}-${colorKey}` : null
|
|
1284
|
+
};
|
|
1285
|
+
};
|
|
1286
|
+
const EdgeLabel = ({ color, label, hasAnnotation, x, y }) => /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs("div", {
|
|
1287
|
+
style: {
|
|
1288
|
+
position: "absolute",
|
|
1289
|
+
transform: `translate(-50%, -50%) translate(${x}px, ${y}px)`,
|
|
1290
|
+
pointerEvents: "all",
|
|
1291
|
+
backgroundColor: color,
|
|
1292
|
+
color: "#ffffff",
|
|
1293
|
+
fontSize: 11,
|
|
1294
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
1295
|
+
fontWeight: 500,
|
|
1296
|
+
padding: "2px 6px",
|
|
1297
|
+
borderRadius: 4,
|
|
1298
|
+
whiteSpace: "nowrap",
|
|
1299
|
+
display: "flex",
|
|
1300
|
+
alignItems: "center",
|
|
1301
|
+
gap: 4
|
|
1302
|
+
},
|
|
1303
|
+
className: "nodrag nopan",
|
|
1304
|
+
children: [label, hasAnnotation && /* @__PURE__ */ jsx(AnnotationBadge, {})]
|
|
1305
|
+
}) });
|
|
1306
|
+
const EdgeEndLabel = ({ label, x, y }) => /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx("div", {
|
|
1307
|
+
style: {
|
|
1308
|
+
position: "absolute",
|
|
1309
|
+
transform: `translate(-50%, -50%) translate(${x}px, ${y}px)`,
|
|
1310
|
+
pointerEvents: "none",
|
|
1311
|
+
color: "#444",
|
|
1312
|
+
fontSize: 10,
|
|
1313
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
1314
|
+
fontWeight: 500,
|
|
1315
|
+
whiteSpace: "nowrap"
|
|
1316
|
+
},
|
|
1317
|
+
className: "nodrag nopan",
|
|
1318
|
+
children: label
|
|
1319
|
+
}) });
|
|
1320
|
+
const END_LABEL_INSET = 22;
|
|
1321
|
+
const END_LABEL_PERP = 9;
|
|
1322
|
+
const endLabelPositions = (start, end) => {
|
|
1323
|
+
const dx = end.x - start.x;
|
|
1324
|
+
const dy = end.y - start.y;
|
|
1325
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
1326
|
+
const ux = dx / length;
|
|
1327
|
+
const uy = dy / length;
|
|
1328
|
+
const px = -uy;
|
|
1329
|
+
const py = ux;
|
|
1330
|
+
return {
|
|
1331
|
+
start: {
|
|
1332
|
+
x: start.x + ux * END_LABEL_INSET + px * END_LABEL_PERP,
|
|
1333
|
+
y: start.y + uy * END_LABEL_INSET + py * END_LABEL_PERP
|
|
1334
|
+
},
|
|
1335
|
+
end: {
|
|
1336
|
+
x: end.x - ux * END_LABEL_INSET + px * END_LABEL_PERP,
|
|
1337
|
+
y: end.y - uy * END_LABEL_INSET + py * END_LABEL_PERP
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
};
|
|
1341
|
+
const GraphFlowEdge = memo((props) => {
|
|
1342
|
+
const { id, source, target, data, sourceX, sourceY, targetX, targetY } = props;
|
|
1343
|
+
const sourceNode = useInternalNode(source);
|
|
1344
|
+
const targetNode = useInternalNode(target);
|
|
1345
|
+
if (!data) return null;
|
|
1346
|
+
const color = data.color ?? "#555";
|
|
1347
|
+
const dashed = data.style?.dashed ?? false;
|
|
1348
|
+
const bold = data.style?.bold ?? false;
|
|
1349
|
+
const startArrow = data.start_arrow ?? null;
|
|
1350
|
+
const endArrow = data.end_arrow ?? null;
|
|
1351
|
+
const { startMarkerId, endMarkerId } = getMarkerIds({
|
|
1352
|
+
color,
|
|
1353
|
+
startArrow,
|
|
1354
|
+
endArrow
|
|
1355
|
+
});
|
|
1356
|
+
const edgePath = resolveEdgePath({
|
|
1357
|
+
edge: props,
|
|
1358
|
+
sourceNode,
|
|
1359
|
+
targetNode
|
|
1360
|
+
});
|
|
1361
|
+
const startPos = data.startPoint ?? {
|
|
1362
|
+
x: sourceX,
|
|
1363
|
+
y: sourceY
|
|
1364
|
+
};
|
|
1365
|
+
const endPos = data.endPoint ?? {
|
|
1366
|
+
x: targetX,
|
|
1367
|
+
y: targetY
|
|
1368
|
+
};
|
|
1369
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1370
|
+
/* @__PURE__ */ jsxs("defs", { children: [endMarkerId && /* @__PURE__ */ jsx(ArrowMarker, {
|
|
1371
|
+
id: endMarkerId,
|
|
1372
|
+
type: endArrow,
|
|
1373
|
+
color,
|
|
1374
|
+
isStart: false
|
|
1375
|
+
}), startMarkerId && /* @__PURE__ */ jsx(ArrowMarker, {
|
|
1376
|
+
id: startMarkerId,
|
|
1377
|
+
type: startArrow,
|
|
1378
|
+
color,
|
|
1379
|
+
isStart: true
|
|
1380
|
+
})] }),
|
|
1381
|
+
/* @__PURE__ */ jsx(BaseEdge, {
|
|
1382
|
+
id,
|
|
1383
|
+
path: edgePath.path,
|
|
1384
|
+
...endMarkerId ? { markerEnd: `url(#${endMarkerId})` } : {},
|
|
1385
|
+
...startMarkerId ? { markerStart: `url(#${startMarkerId})` } : {},
|
|
1386
|
+
style: {
|
|
1387
|
+
stroke: color,
|
|
1388
|
+
strokeWidth: bold ? 3 : 1.5,
|
|
1389
|
+
strokeDasharray: dashed ? "6 3" : void 0
|
|
1390
|
+
}
|
|
1391
|
+
}),
|
|
1392
|
+
(data.label || data.annotation) && /* @__PURE__ */ jsx(EdgeLabel, {
|
|
1393
|
+
color,
|
|
1394
|
+
label: data.label,
|
|
1395
|
+
hasAnnotation: Boolean(data.annotation),
|
|
1396
|
+
x: edgePath.labelX,
|
|
1397
|
+
y: edgePath.labelY
|
|
1398
|
+
}),
|
|
1399
|
+
(data.start_label || data.end_label) && (() => {
|
|
1400
|
+
const positions = endLabelPositions(startPos, endPos);
|
|
1401
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [data.start_label && /* @__PURE__ */ jsx(EdgeEndLabel, {
|
|
1402
|
+
label: data.start_label,
|
|
1403
|
+
x: positions.start.x,
|
|
1404
|
+
y: positions.start.y
|
|
1405
|
+
}), data.end_label && /* @__PURE__ */ jsx(EdgeEndLabel, {
|
|
1406
|
+
label: data.end_label,
|
|
1407
|
+
x: positions.end.x,
|
|
1408
|
+
y: positions.end.y
|
|
1409
|
+
})] });
|
|
1410
|
+
})()
|
|
1411
|
+
] });
|
|
1412
|
+
});
|
|
1413
|
+
GraphFlowEdge.displayName = "GraphFlowEdge";
|
|
1414
|
+
//#endregion
|
|
1415
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/utils/bounds.ts
|
|
1416
|
+
function nodeBoundsFromData(nodes) {
|
|
1417
|
+
if (nodes.length === 0) return {
|
|
1418
|
+
x: 0,
|
|
1419
|
+
y: 0,
|
|
1420
|
+
width: 0,
|
|
1421
|
+
height: 0
|
|
1422
|
+
};
|
|
1423
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1424
|
+
for (const node of nodes) {
|
|
1425
|
+
const x = node.position.x;
|
|
1426
|
+
const y = node.position.y;
|
|
1427
|
+
const w = node.measured?.width ?? 0;
|
|
1428
|
+
const h = node.measured?.height ?? 0;
|
|
1429
|
+
minX = Math.min(minX, x);
|
|
1430
|
+
minY = Math.min(minY, y);
|
|
1431
|
+
maxX = Math.max(maxX, x + w);
|
|
1432
|
+
maxY = Math.max(maxY, y + h);
|
|
1433
|
+
}
|
|
1434
|
+
return Number.isFinite(minX) ? {
|
|
1435
|
+
x: minX,
|
|
1436
|
+
y: minY,
|
|
1437
|
+
width: maxX - minX,
|
|
1438
|
+
height: maxY - minY
|
|
1439
|
+
} : {
|
|
1440
|
+
x: 0,
|
|
1441
|
+
y: 0,
|
|
1442
|
+
width: 0,
|
|
1443
|
+
height: 0
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function svgPathBounds(d) {
|
|
1447
|
+
const nums = (d.match(/-?[\d.]+(?:e[+-]?\d+)?/gi) ?? []).map(parseFloat);
|
|
1448
|
+
if (nums.length < 2) return null;
|
|
1449
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1450
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
1451
|
+
minX = Math.min(minX, nums[i]);
|
|
1452
|
+
minY = Math.min(minY, nums[i + 1]);
|
|
1453
|
+
maxX = Math.max(maxX, nums[i]);
|
|
1454
|
+
maxY = Math.max(maxY, nums[i + 1]);
|
|
1455
|
+
}
|
|
1456
|
+
return Number.isFinite(minX) ? {
|
|
1457
|
+
x: minX,
|
|
1458
|
+
y: minY,
|
|
1459
|
+
width: maxX - minX,
|
|
1460
|
+
height: maxY - minY
|
|
1461
|
+
} : null;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Computes content bounds from graph data without touching the DOM.
|
|
1465
|
+
* Uses node positions/sizes and pre-computed ELK edge paths.
|
|
1466
|
+
* Safe to call immediately after layout with no timing concerns.
|
|
1467
|
+
*/
|
|
1468
|
+
function computeAutoFitBounds(nodes, edges) {
|
|
1469
|
+
let combined = nodeBoundsFromData(nodes);
|
|
1470
|
+
for (const edge of edges) {
|
|
1471
|
+
const path = edge.data?.path;
|
|
1472
|
+
if (!path) continue;
|
|
1473
|
+
const eb = svgPathBounds(path);
|
|
1474
|
+
if (eb) combined = unionRects(combined, eb);
|
|
1475
|
+
}
|
|
1476
|
+
return combined;
|
|
1477
|
+
}
|
|
1478
|
+
function unionRects(a, b) {
|
|
1479
|
+
const x = Math.min(a.x, b.x);
|
|
1480
|
+
const y = Math.min(a.y, b.y);
|
|
1481
|
+
const right = Math.max(a.x + a.width, b.x + b.width);
|
|
1482
|
+
const bottom = Math.max(a.y + a.height, b.y + b.height);
|
|
1483
|
+
return {
|
|
1484
|
+
x,
|
|
1485
|
+
y,
|
|
1486
|
+
width: right - x,
|
|
1487
|
+
height: bottom - y
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Reads the viewport transform directly from the inline style React Flow writes:
|
|
1492
|
+
* `transform: translate(Xpx, Ypx) scale(Z)`
|
|
1493
|
+
*/
|
|
1494
|
+
function parseViewportTransform(viewportEl) {
|
|
1495
|
+
const match = (viewportEl.style.transform ?? "").match(/translate\((-?[\d.]+)px,\s*(-?[\d.]+)px\)\s*scale\((-?[\d.]+)\)/);
|
|
1496
|
+
if (!match) return null;
|
|
1497
|
+
return {
|
|
1498
|
+
x: parseFloat(match[1]),
|
|
1499
|
+
y: parseFloat(match[2]),
|
|
1500
|
+
zoom: parseFloat(match[3])
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Returns the bounding rect (in flow coordinates) of all nodes plus all
|
|
1505
|
+
* rendered edge paths. React Flow's built-in helpers only account for nodes;
|
|
1506
|
+
* curved or long edges can extend well outside the node bounds, causing them
|
|
1507
|
+
* to be clipped in exports and in fitView.
|
|
1508
|
+
*
|
|
1509
|
+
* In @xyflow/react v12 each edge is wrapped in its own <svg> (position:
|
|
1510
|
+
* absolute), so getBBox() coordinates are not reliably in flow space. Instead
|
|
1511
|
+
* we use getBoundingClientRect() on each .react-flow__edge-path element and
|
|
1512
|
+
* convert from screen coords back to flow coords via the inverse of the
|
|
1513
|
+
* viewport's CSS transform.
|
|
1514
|
+
*/
|
|
1515
|
+
function getContentBounds(nodes, viewportEl, getNodesBounds) {
|
|
1516
|
+
const nodeBounds = getNodesBounds(nodes);
|
|
1517
|
+
const vt = parseViewportTransform(viewportEl);
|
|
1518
|
+
if (!vt || vt.zoom === 0) return nodeBounds;
|
|
1519
|
+
const containerRect = viewportEl.parentElement?.getBoundingClientRect();
|
|
1520
|
+
if (!containerRect) return nodeBounds;
|
|
1521
|
+
const edgePaths = viewportEl.querySelectorAll(".react-flow__edge-path");
|
|
1522
|
+
if (edgePaths.length === 0) return nodeBounds;
|
|
1523
|
+
let combined = nodeBounds;
|
|
1524
|
+
for (const path of Array.from(edgePaths)) {
|
|
1525
|
+
const rect = path.getBoundingClientRect();
|
|
1526
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
1527
|
+
const flowX = (rect.left - containerRect.left - vt.x) / vt.zoom;
|
|
1528
|
+
const flowY = (rect.top - containerRect.top - vt.y) / vt.zoom;
|
|
1529
|
+
const flowRight = (rect.right - containerRect.left - vt.x) / vt.zoom;
|
|
1530
|
+
const flowBottom = (rect.bottom - containerRect.top - vt.y) / vt.zoom;
|
|
1531
|
+
combined = unionRects(combined, {
|
|
1532
|
+
x: flowX,
|
|
1533
|
+
y: flowY,
|
|
1534
|
+
width: flowRight - flowX,
|
|
1535
|
+
height: flowBottom - flowY
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
return combined;
|
|
1539
|
+
}
|
|
1540
|
+
//#endregion
|
|
1541
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/utils/download.ts
|
|
1542
|
+
const MAX_PX = 2048;
|
|
1543
|
+
const EXPORT_PADDING = .05;
|
|
1544
|
+
function buildCaptureOptions(imageWidth, imageHeight, x, y, zoom) {
|
|
1545
|
+
return {
|
|
1546
|
+
backgroundColor: "#ffffff",
|
|
1547
|
+
width: imageWidth,
|
|
1548
|
+
height: imageHeight,
|
|
1549
|
+
skipFonts: true,
|
|
1550
|
+
style: {
|
|
1551
|
+
width: `${imageWidth}px`,
|
|
1552
|
+
height: `${imageHeight}px`,
|
|
1553
|
+
transform: `translate(${x}px, ${y}px) scale(${zoom})`
|
|
1554
|
+
},
|
|
1555
|
+
filter: (node) => !(node instanceof Element) || !node.classList.contains("react-flow__panel") && !node.classList.contains("react-flow__background")
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function resolveViewport(container, nodes, getNodesBounds) {
|
|
1559
|
+
if (nodes.length === 0) return null;
|
|
1560
|
+
const viewportEl = container.querySelector(".react-flow__viewport");
|
|
1561
|
+
if (!viewportEl) return null;
|
|
1562
|
+
const bounds = getContentBounds(nodes, viewportEl, getNodesBounds);
|
|
1563
|
+
const padX = bounds.width * EXPORT_PADDING;
|
|
1564
|
+
const padY = bounds.height * EXPORT_PADDING;
|
|
1565
|
+
const paddedWidth = bounds.width + 2 * padX;
|
|
1566
|
+
const paddedHeight = bounds.height + 2 * padY;
|
|
1567
|
+
const zoom = Math.min(MAX_PX / paddedWidth, MAX_PX / paddedHeight);
|
|
1568
|
+
return {
|
|
1569
|
+
viewportEl,
|
|
1570
|
+
imageWidth: Math.round(paddedWidth * zoom),
|
|
1571
|
+
imageHeight: Math.round(paddedHeight * zoom),
|
|
1572
|
+
x: (padX - bounds.x) * zoom,
|
|
1573
|
+
y: (padY - bounds.y) * zoom,
|
|
1574
|
+
zoom
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
async function downloadPng(container, nodes, getNodesBounds) {
|
|
1578
|
+
const capture = resolveViewport(container, nodes, getNodesBounds);
|
|
1579
|
+
if (!capture) return;
|
|
1580
|
+
const { viewportEl, imageWidth, imageHeight, x, y, zoom } = capture;
|
|
1581
|
+
saveAs(await toPng(viewportEl, buildCaptureOptions(imageWidth, imageHeight, x, y, zoom)), "graph.png");
|
|
1582
|
+
}
|
|
1583
|
+
async function downloadSvg(container, nodes, getNodesBounds) {
|
|
1584
|
+
const capture = resolveViewport(container, nodes, getNodesBounds);
|
|
1585
|
+
if (!capture) return;
|
|
1586
|
+
const { viewportEl, imageWidth, imageHeight, x, y, zoom } = capture;
|
|
1587
|
+
saveAs(await toSvg(viewportEl, buildCaptureOptions(imageWidth, imageHeight, x, y, zoom)), "graph.svg");
|
|
1588
|
+
}
|
|
1589
|
+
async function downloadPdf(container, nodes, getNodesBounds) {
|
|
1590
|
+
const capture = resolveViewport(container, nodes, getNodesBounds);
|
|
1591
|
+
if (!capture) return;
|
|
1592
|
+
const { viewportEl, imageWidth, imageHeight, x, y, zoom } = capture;
|
|
1593
|
+
const dataUrl = await toPng(viewportEl, buildCaptureOptions(imageWidth, imageHeight, x, y, zoom));
|
|
1594
|
+
const pdf = new jsPDF({
|
|
1595
|
+
orientation: imageWidth >= imageHeight ? "landscape" : "portrait",
|
|
1596
|
+
unit: "px",
|
|
1597
|
+
format: [imageWidth, imageHeight]
|
|
1598
|
+
});
|
|
1599
|
+
pdf.addImage(dataUrl, "PNG", 0, 0, imageWidth, imageHeight);
|
|
1600
|
+
pdf.save("graph.pdf");
|
|
1601
|
+
}
|
|
1602
|
+
//#endregion
|
|
1603
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/index.tsx
|
|
1604
|
+
const nodeTypes = { node: GraphFlowNode };
|
|
1605
|
+
const edgeTypes = { graphflow: GraphFlowEdge };
|
|
1606
|
+
const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
1607
|
+
const { nodes, edges, layoutReady, error, onNodesChange } = useLayout(visualization);
|
|
1608
|
+
const { fitBounds, getNodes, getEdges, getNodesBounds } = useReactFlow();
|
|
1609
|
+
const wrapperRef = useRef(null);
|
|
1610
|
+
const fitViewWithEdges = useCallback(() => {
|
|
1611
|
+
fitBounds(computeAutoFitBounds(getNodes(), getEdges()), { padding: FIT_VIEW_PADDING });
|
|
1612
|
+
}, [
|
|
1613
|
+
fitBounds,
|
|
1614
|
+
getNodes,
|
|
1615
|
+
getEdges
|
|
1616
|
+
]);
|
|
1617
|
+
const handleDownloadPng = useCallback(() => wrapperRef.current && downloadPng(wrapperRef.current, getNodes(), getNodesBounds), [getNodes, getNodesBounds]);
|
|
1618
|
+
const handleDownloadSvg = useCallback(() => wrapperRef.current && downloadSvg(wrapperRef.current, getNodes(), getNodesBounds), [getNodes, getNodesBounds]);
|
|
1619
|
+
const handleDownloadPdf = useCallback(() => wrapperRef.current && downloadPdf(wrapperRef.current, getNodes(), getNodesBounds), [getNodes, getNodesBounds]);
|
|
1620
|
+
const wasReadyRef = useRef(false);
|
|
1621
|
+
useEffect(() => {
|
|
1622
|
+
if (!layoutReady) {
|
|
1623
|
+
wasReadyRef.current = false;
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (wasReadyRef.current) return;
|
|
1627
|
+
wasReadyRef.current = true;
|
|
1628
|
+
fitViewWithEdges();
|
|
1629
|
+
}, [layoutReady, fitViewWithEdges]);
|
|
1630
|
+
if (error) return /* @__PURE__ */ jsx(Stack, {
|
|
1631
|
+
h: "100%",
|
|
1632
|
+
justify: "center",
|
|
1633
|
+
p: "md",
|
|
1634
|
+
children: /* @__PURE__ */ jsx(Alert, {
|
|
1635
|
+
color: "red",
|
|
1636
|
+
title: error.message,
|
|
1637
|
+
children: error.details?.map((detail) => /* @__PURE__ */ jsx(Code, {
|
|
1638
|
+
display: "block",
|
|
1639
|
+
mb: 4,
|
|
1640
|
+
children: detail
|
|
1641
|
+
}, detail))
|
|
582
1642
|
})
|
|
583
1643
|
});
|
|
1644
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LoadingOverlay, {
|
|
1645
|
+
visible: !layoutReady,
|
|
1646
|
+
zIndex: 10
|
|
1647
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1648
|
+
ref: wrapperRef,
|
|
1649
|
+
style: {
|
|
1650
|
+
width: "100%",
|
|
1651
|
+
height: "100%",
|
|
1652
|
+
opacity: layoutReady ? 1 : 0
|
|
1653
|
+
},
|
|
1654
|
+
children: /* @__PURE__ */ jsx(ReactFlow, {
|
|
1655
|
+
nodes,
|
|
1656
|
+
edges,
|
|
1657
|
+
nodeTypes,
|
|
1658
|
+
edgeTypes,
|
|
1659
|
+
onNodesChange,
|
|
1660
|
+
nodesDraggable: false,
|
|
1661
|
+
nodesConnectable: false,
|
|
1662
|
+
elementsSelectable: false,
|
|
1663
|
+
zoomOnScroll: !isPreview,
|
|
1664
|
+
panOnDrag: !isPreview,
|
|
1665
|
+
minZoom: .05,
|
|
1666
|
+
maxZoom: 3,
|
|
1667
|
+
children: !isPreview && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Background, {
|
|
1668
|
+
color: "#e5e7eb",
|
|
1669
|
+
gap: 20,
|
|
1670
|
+
size: 1
|
|
1671
|
+
}), /* @__PURE__ */ jsxs(Controls, {
|
|
1672
|
+
showInteractive: false,
|
|
1673
|
+
showFitView: false,
|
|
1674
|
+
children: [
|
|
1675
|
+
/* @__PURE__ */ jsx(ControlButton, {
|
|
1676
|
+
onClick: fitViewWithEdges,
|
|
1677
|
+
title: "fit view",
|
|
1678
|
+
children: /* @__PURE__ */ jsx(Maximize2Icon, { size: 11 })
|
|
1679
|
+
}),
|
|
1680
|
+
/* @__PURE__ */ jsxs(Menu, {
|
|
1681
|
+
position: "right",
|
|
1682
|
+
withinPortal: false,
|
|
1683
|
+
children: [/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
|
|
1684
|
+
variant: "transparent",
|
|
1685
|
+
children: /* @__PURE__ */ jsx(DownloadIcon, {
|
|
1686
|
+
color: "black",
|
|
1687
|
+
size: 18
|
|
1688
|
+
})
|
|
1689
|
+
}) }), /* @__PURE__ */ jsxs(Menu.Dropdown, { children: [
|
|
1690
|
+
/* @__PURE__ */ jsx(Menu.Item, {
|
|
1691
|
+
onClick: handleDownloadPng,
|
|
1692
|
+
children: "PNG"
|
|
1693
|
+
}),
|
|
1694
|
+
/* @__PURE__ */ jsx(Menu.Item, {
|
|
1695
|
+
onClick: handleDownloadSvg,
|
|
1696
|
+
children: "SVG"
|
|
1697
|
+
}),
|
|
1698
|
+
/* @__PURE__ */ jsx(Menu.Item, {
|
|
1699
|
+
onClick: handleDownloadPdf,
|
|
1700
|
+
children: "PDF"
|
|
1701
|
+
})
|
|
1702
|
+
] })]
|
|
1703
|
+
}),
|
|
1704
|
+
menuItems?.map((menuItem) => /* @__PURE__ */ jsx(ControlButton, {
|
|
1705
|
+
onClick: menuItem.onClick,
|
|
1706
|
+
title: menuItem.label,
|
|
1707
|
+
children: menuItem.icon
|
|
1708
|
+
}))
|
|
1709
|
+
]
|
|
1710
|
+
})] })
|
|
1711
|
+
})
|
|
1712
|
+
})] });
|
|
584
1713
|
};
|
|
585
|
-
|
|
586
|
-
|
|
1714
|
+
const GraphFlow = (props) => /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(GraphFlowCanvas, { ...props }) });
|
|
1715
|
+
//#endregion
|
|
1716
|
+
//#region src/components/ResourceView/Viewers/Graph.tsx
|
|
1717
|
+
const GraphViewer = (props) => /* @__PURE__ */ jsx(Box, {
|
|
1718
|
+
h: "100%",
|
|
1719
|
+
w: "100%",
|
|
1720
|
+
pos: "relative",
|
|
1721
|
+
style: { overflow: "hidden" },
|
|
1722
|
+
children: /* @__PURE__ */ jsx(GraphFlow, { ...props })
|
|
1723
|
+
});
|
|
587
1724
|
//#endregion
|
|
588
1725
|
//#region src/components/ResourceView/Viewers/Plotly.tsx
|
|
589
1726
|
const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
|
|
@@ -605,22 +1742,80 @@ const PlotlyViewer = ({ visualization, isPreview }) => {
|
|
|
605
1742
|
}
|
|
606
1743
|
});
|
|
607
1744
|
};
|
|
608
|
-
|
|
609
|
-
|
|
1745
|
+
//#endregion
|
|
1746
|
+
//#region src/util/dayjs.ts
|
|
1747
|
+
dayjs.extend(isBetween);
|
|
1748
|
+
var dayjs_default = dayjs;
|
|
1749
|
+
const formatDateTime = (dateString) => dayjs(dateString).format("YYYY-MM-DD HH:mm");
|
|
1750
|
+
const formatDate = (dateString) => dayjs(dateString).format("YYYY-MM-DD");
|
|
1751
|
+
//#endregion
|
|
1752
|
+
//#region src/components/ResourceView/Viewers/Table.tsx
|
|
1753
|
+
const formatCell = (value, dataType) => {
|
|
1754
|
+
if (value == null) return "";
|
|
1755
|
+
switch (dataType) {
|
|
1756
|
+
case "boolean": return value ? "✅" : "❌";
|
|
1757
|
+
case "date": return dayjs_default(value).isValid() ? formatDate(value) : String(value);
|
|
1758
|
+
case "datetime": return dayjs_default(value).isValid() ? formatDateTime(value) : String(value);
|
|
1759
|
+
default: return String(value);
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
const PAGE_SIZES = [
|
|
1763
|
+
20,
|
|
1764
|
+
50,
|
|
1765
|
+
75
|
|
1766
|
+
];
|
|
1767
|
+
const TableView = ({ visualization }) => {
|
|
1768
|
+
const [pageSize, setPageSize] = useState(PAGE_SIZES[0]);
|
|
1769
|
+
const columns = useMemo(() => visualization.columns.map((col) => ({
|
|
1770
|
+
accessor: col.id,
|
|
1771
|
+
title: col.label ?? col.id,
|
|
1772
|
+
render: (record) => formatCell(record[col.id], col.data_type)
|
|
1773
|
+
})), [visualization.columns]);
|
|
1774
|
+
const [page, setPage] = useState(1);
|
|
1775
|
+
const [records, setRecords] = useState(visualization.rows.slice(0, pageSize));
|
|
1776
|
+
useEffect(() => {
|
|
1777
|
+
const from = (page - 1) * pageSize;
|
|
1778
|
+
const to = from + pageSize;
|
|
1779
|
+
setRecords(visualization.rows.slice(from, to));
|
|
1780
|
+
}, [
|
|
1781
|
+
page,
|
|
1782
|
+
pageSize,
|
|
1783
|
+
visualization
|
|
1784
|
+
]);
|
|
1785
|
+
return /* @__PURE__ */ jsx(DataTable, {
|
|
1786
|
+
records,
|
|
1787
|
+
columns,
|
|
1788
|
+
withTableBorder: true,
|
|
1789
|
+
withColumnBorders: true,
|
|
1790
|
+
withRowBorders: true,
|
|
1791
|
+
...visualization.rows.length > PAGE_SIZES[0] ? {
|
|
1792
|
+
totalRecords: visualization.rows.length,
|
|
1793
|
+
recordsPerPage: pageSize,
|
|
1794
|
+
onPageChange: (p) => setPage(p),
|
|
1795
|
+
page,
|
|
1796
|
+
recordsPerPageOptions: [...PAGE_SIZES],
|
|
1797
|
+
onRecordsPerPageChange: (newPageSize) => {
|
|
1798
|
+
setPageSize(newPageSize);
|
|
1799
|
+
setPage(1);
|
|
1800
|
+
}
|
|
1801
|
+
} : {}
|
|
1802
|
+
});
|
|
1803
|
+
};
|
|
610
1804
|
//#endregion
|
|
611
1805
|
//#region src/components/ResourceView/index.tsx
|
|
612
|
-
const
|
|
613
|
-
table:
|
|
614
|
-
svg:
|
|
615
|
-
graph:
|
|
616
|
-
dot:
|
|
617
|
-
plotly:
|
|
618
|
-
};
|
|
619
|
-
const Visualization = ({ visualization, isPreview = false }) => {
|
|
620
|
-
const Component =
|
|
1806
|
+
const visualizationMap = {
|
|
1807
|
+
table: TableView,
|
|
1808
|
+
svg: SvgViewer,
|
|
1809
|
+
graph: GraphViewer,
|
|
1810
|
+
dot: DotToSvgViewer,
|
|
1811
|
+
plotly: PlotlyViewer
|
|
1812
|
+
};
|
|
1813
|
+
const Visualization = ({ visualization, isPreview = false, menuItems }) => {
|
|
1814
|
+
const Component = visualizationMap[visualization.type];
|
|
621
1815
|
return /* @__PURE__ */ jsx(Component, {
|
|
622
1816
|
visualization,
|
|
623
|
-
isPreview
|
|
1817
|
+
isPreview,
|
|
1818
|
+
...menuItems && { menuItems }
|
|
624
1819
|
});
|
|
625
1820
|
};
|
|
626
1821
|
const ResourceViewer = ({ id, isPreview = false }) => {
|
|
@@ -646,7 +1841,6 @@ const ResourceViewer = ({ id, isPreview = false }) => {
|
|
|
646
1841
|
isPreview
|
|
647
1842
|
});
|
|
648
1843
|
};
|
|
649
|
-
|
|
650
1844
|
//#endregion
|
|
651
1845
|
//#region src/components/ResourceModal/ResourceModal.tsx
|
|
652
1846
|
const ResourceModal = ({ id, onClose }) => {
|
|
@@ -689,6 +1883,18 @@ const ResourceModal = ({ id, onClose }) => {
|
|
|
689
1883
|
})
|
|
690
1884
|
});
|
|
691
1885
|
};
|
|
692
|
-
|
|
693
1886
|
//#endregion
|
|
694
|
-
|
|
1887
|
+
//#region src/components/ResourceSelect/ResourceSelect.tsx
|
|
1888
|
+
const ResourceSelect = ({ type, value, ...props }) => {
|
|
1889
|
+
const { data: resources = [] } = useResources({ resource_type: type });
|
|
1890
|
+
return /* @__PURE__ */ jsx(Select, {
|
|
1891
|
+
...props,
|
|
1892
|
+
value: value ?? null,
|
|
1893
|
+
data: resources.map(({ name, id }) => ({
|
|
1894
|
+
value: id,
|
|
1895
|
+
label: name
|
|
1896
|
+
}))
|
|
1897
|
+
});
|
|
1898
|
+
};
|
|
1899
|
+
//#endregion
|
|
1900
|
+
export { ResourceModal, ResourceSelect, ResourceViewer, Visualization };
|