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