@ocelescope/resources 0.1.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -6
- package/dist/index.js +1058 -1091
- package/package.json +13 -15
package/dist/index.js
CHANGED
|
@@ -2,19 +2,20 @@ import { ActionIcon, Alert, Badge, Box, Button, ButtonGroup, Code, Group, Loadin
|
|
|
2
2
|
import { useResource, useResources } from "@ocelescope/api-base";
|
|
3
3
|
import { DownloadIcon, EyeOffIcon, Maximize2Icon, MaximizeIcon, MinusIcon, PlusIcon, XIcon } from "lucide-react";
|
|
4
4
|
import { Graphviz } from "@hpcc-js/wasm-graphviz";
|
|
5
|
-
import
|
|
5
|
+
import FileSaver from "file-saver";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
7
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
8
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
|
8
|
-
import {
|
|
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";
|
|
9
|
+
import { Background, BaseEdge, ControlButton, Controls, EdgeLabelRenderer, Handle, Position, ReactFlow, ReactFlowProvider, applyNodeChanges, getBezierPath, getSmoothStepPath, useInternalNode, useReactFlow } from "@xyflow/react";
|
|
11
10
|
import { toPng, toSvg } from "html-to-image";
|
|
12
11
|
import { jsPDF } from "jspdf";
|
|
12
|
+
import ELK from "elkjs/lib/elk.bundled.js";
|
|
13
13
|
import dynamic from "next/dynamic.js";
|
|
14
14
|
import { DataTable } from "mantine-datatable";
|
|
15
15
|
import dayjs from "dayjs";
|
|
16
16
|
import isBetween from "dayjs/plugin/isBetween.js";
|
|
17
17
|
//#region src/components/ResourceView/Viewers/SVG.tsx
|
|
18
|
+
const { saveAs: saveAs$2 } = FileSaver;
|
|
18
19
|
const SvgPanWrapper = ({ children, onDownload }) => {
|
|
19
20
|
return /* @__PURE__ */ jsx(Box, {
|
|
20
21
|
w: "100%",
|
|
@@ -69,7 +70,8 @@ const SvgViewer = ({ visualization, isPreview }) => {
|
|
|
69
70
|
const cleanedSvg = visualization.svg.replace(/width="[^"]+"/, "width=\"100%\"").replace(/height="[^"]+"/, "height=\"100%\"").replace(/<svg(?![^>]*xmlns=)/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
|
|
70
71
|
const handleDownload = () => {
|
|
71
72
|
if (!cleanedSvg) return;
|
|
72
|
-
|
|
73
|
+
const blob = new Blob([cleanedSvg], { type: "image/svg+xml;charset=utf-8" });
|
|
74
|
+
saveAs$2(blob, "resource.svg");
|
|
73
75
|
};
|
|
74
76
|
const content = /* @__PURE__ */ jsx("div", {
|
|
75
77
|
style: {
|
|
@@ -86,6 +88,7 @@ const SvgViewer = ({ visualization, isPreview }) => {
|
|
|
86
88
|
};
|
|
87
89
|
//#endregion
|
|
88
90
|
//#region src/components/ResourceView/Viewers/Dot.tsx
|
|
91
|
+
const { saveAs: saveAs$1 } = FileSaver;
|
|
89
92
|
const DotToSvgViewer = ({ isPreview, visualization }) => {
|
|
90
93
|
const [svg, setSvg] = useState(null);
|
|
91
94
|
const [error, setError] = useState(null);
|
|
@@ -114,7 +117,8 @@ const DotToSvgViewer = ({ isPreview, visualization }) => {
|
|
|
114
117
|
}, [svg]);
|
|
115
118
|
const handleDownload = () => {
|
|
116
119
|
if (!cleanedSvg) return;
|
|
117
|
-
|
|
120
|
+
const blob = new Blob([cleanedSvg], { type: "image/svg+xml;charset=utf-8" });
|
|
121
|
+
saveAs$1(blob, "resource.svg");
|
|
118
122
|
};
|
|
119
123
|
const content = /* @__PURE__ */ jsxs("div", {
|
|
120
124
|
style: {
|
|
@@ -156,15 +160,283 @@ const DotToSvgViewer = ({ isPreview, visualization }) => {
|
|
|
156
160
|
});
|
|
157
161
|
};
|
|
158
162
|
//#endregion
|
|
163
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/GraphControls.tsx
|
|
164
|
+
const GraphControls = ({ onFitView, onDownloadPng, onDownloadSvg, onDownloadPdf, menuItems, isPreview }) => {
|
|
165
|
+
if (isPreview) return null;
|
|
166
|
+
return /* @__PURE__ */ jsxs(Controls, {
|
|
167
|
+
showInteractive: false,
|
|
168
|
+
showFitView: false,
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ jsx(ControlButton, {
|
|
171
|
+
onClick: onFitView,
|
|
172
|
+
title: "fit view",
|
|
173
|
+
children: /* @__PURE__ */ jsx(Maximize2Icon, { size: 11 })
|
|
174
|
+
}),
|
|
175
|
+
/* @__PURE__ */ jsxs(Menu, {
|
|
176
|
+
position: "right",
|
|
177
|
+
withinPortal: false,
|
|
178
|
+
children: [/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
|
|
179
|
+
variant: "transparent",
|
|
180
|
+
children: /* @__PURE__ */ jsx(DownloadIcon, {
|
|
181
|
+
color: "black",
|
|
182
|
+
size: 18
|
|
183
|
+
})
|
|
184
|
+
}) }), /* @__PURE__ */ jsxs(Menu.Dropdown, { children: [
|
|
185
|
+
/* @__PURE__ */ jsx(Menu.Item, {
|
|
186
|
+
onClick: onDownloadPng,
|
|
187
|
+
children: "PNG"
|
|
188
|
+
}),
|
|
189
|
+
/* @__PURE__ */ jsx(Menu.Item, {
|
|
190
|
+
onClick: onDownloadSvg,
|
|
191
|
+
children: "SVG"
|
|
192
|
+
}),
|
|
193
|
+
/* @__PURE__ */ jsx(Menu.Item, {
|
|
194
|
+
onClick: onDownloadPdf,
|
|
195
|
+
children: "PDF"
|
|
196
|
+
})
|
|
197
|
+
] })]
|
|
198
|
+
}),
|
|
199
|
+
menuItems?.map((menuItem) => /* @__PURE__ */ jsx(ControlButton, {
|
|
200
|
+
onClick: menuItem.onClick,
|
|
201
|
+
title: menuItem.label,
|
|
202
|
+
children: menuItem.icon
|
|
203
|
+
}, menuItem.label))
|
|
204
|
+
]
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/utils/bounds.ts
|
|
209
|
+
function nodeBoundsFromData(nodes) {
|
|
210
|
+
if (nodes.length === 0) return {
|
|
211
|
+
x: 0,
|
|
212
|
+
y: 0,
|
|
213
|
+
width: 0,
|
|
214
|
+
height: 0
|
|
215
|
+
};
|
|
216
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
217
|
+
for (const node of nodes) {
|
|
218
|
+
const x = node.position.x;
|
|
219
|
+
const y = node.position.y;
|
|
220
|
+
const w = node.measured?.width ?? 0;
|
|
221
|
+
const h = node.measured?.height ?? 0;
|
|
222
|
+
minX = Math.min(minX, x);
|
|
223
|
+
minY = Math.min(minY, y);
|
|
224
|
+
maxX = Math.max(maxX, x + w);
|
|
225
|
+
maxY = Math.max(maxY, y + h);
|
|
226
|
+
}
|
|
227
|
+
return Number.isFinite(minX) ? {
|
|
228
|
+
x: minX,
|
|
229
|
+
y: minY,
|
|
230
|
+
width: maxX - minX,
|
|
231
|
+
height: maxY - minY
|
|
232
|
+
} : {
|
|
233
|
+
x: 0,
|
|
234
|
+
y: 0,
|
|
235
|
+
width: 0,
|
|
236
|
+
height: 0
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function svgPathBounds(d) {
|
|
240
|
+
const nums = (d.match(/-?[\d.]+(?:e[+-]?\d+)?/gi) ?? []).map(parseFloat);
|
|
241
|
+
if (nums.length < 2) return null;
|
|
242
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
243
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
244
|
+
minX = Math.min(minX, nums[i]);
|
|
245
|
+
minY = Math.min(minY, nums[i + 1]);
|
|
246
|
+
maxX = Math.max(maxX, nums[i]);
|
|
247
|
+
maxY = Math.max(maxY, nums[i + 1]);
|
|
248
|
+
}
|
|
249
|
+
return Number.isFinite(minX) ? {
|
|
250
|
+
x: minX,
|
|
251
|
+
y: minY,
|
|
252
|
+
width: maxX - minX,
|
|
253
|
+
height: maxY - minY
|
|
254
|
+
} : null;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Computes content bounds from graph data without touching the DOM.
|
|
258
|
+
* Uses node positions/sizes and pre-computed ELK edge paths.
|
|
259
|
+
* Safe to call immediately after layout with no timing concerns.
|
|
260
|
+
*/
|
|
261
|
+
function computeAutoFitBounds(nodes, edges) {
|
|
262
|
+
let combined = nodeBoundsFromData(nodes);
|
|
263
|
+
for (const edge of edges) {
|
|
264
|
+
const path = edge.data?.layout?.path;
|
|
265
|
+
if (!path) continue;
|
|
266
|
+
const eb = svgPathBounds(path);
|
|
267
|
+
if (eb) combined = unionRects(combined, eb);
|
|
268
|
+
}
|
|
269
|
+
return combined;
|
|
270
|
+
}
|
|
271
|
+
function unionRects(a, b) {
|
|
272
|
+
const x = Math.min(a.x, b.x);
|
|
273
|
+
const y = Math.min(a.y, b.y);
|
|
274
|
+
const right = Math.max(a.x + a.width, b.x + b.width);
|
|
275
|
+
const bottom = Math.max(a.y + a.height, b.y + b.height);
|
|
276
|
+
return {
|
|
277
|
+
x,
|
|
278
|
+
y,
|
|
279
|
+
width: right - x,
|
|
280
|
+
height: bottom - y
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Reads the viewport transform directly from the inline style React Flow writes:
|
|
285
|
+
* `transform: translate(Xpx, Ypx) scale(Z)`
|
|
286
|
+
*/
|
|
287
|
+
function parseViewportTransform(viewportEl) {
|
|
288
|
+
const match = (viewportEl.style.transform ?? "").match(/translate\((-?[\d.]+)px,\s*(-?[\d.]+)px\)\s*scale\((-?[\d.]+)\)/);
|
|
289
|
+
if (!match) return null;
|
|
290
|
+
return {
|
|
291
|
+
x: parseFloat(match[1]),
|
|
292
|
+
y: parseFloat(match[2]),
|
|
293
|
+
zoom: parseFloat(match[3])
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Returns the bounding rect (in flow coordinates) of all nodes plus all
|
|
298
|
+
* rendered edge paths. React Flow's built-in helpers only account for nodes;
|
|
299
|
+
* curved or long edges can extend well outside the node bounds, causing them
|
|
300
|
+
* to be clipped in exports and in fitView.
|
|
301
|
+
*
|
|
302
|
+
* In @xyflow/react v12 each edge is wrapped in its own <svg> (position:
|
|
303
|
+
* absolute), so getBBox() coordinates are not reliably in flow space. Instead
|
|
304
|
+
* we use getBoundingClientRect() on each .react-flow__edge-path element and
|
|
305
|
+
* convert from screen coords back to flow coords via the inverse of the
|
|
306
|
+
* viewport's CSS transform.
|
|
307
|
+
*/
|
|
308
|
+
function getContentBounds(nodes, viewportEl, getNodesBounds) {
|
|
309
|
+
const nodeBounds = getNodesBounds(nodes);
|
|
310
|
+
const vt = parseViewportTransform(viewportEl);
|
|
311
|
+
if (!vt || vt.zoom === 0) return nodeBounds;
|
|
312
|
+
const containerRect = viewportEl.parentElement?.getBoundingClientRect();
|
|
313
|
+
if (!containerRect) return nodeBounds;
|
|
314
|
+
const edgePaths = viewportEl.querySelectorAll(".react-flow__edge-path");
|
|
315
|
+
if (edgePaths.length === 0) return nodeBounds;
|
|
316
|
+
let combined = nodeBounds;
|
|
317
|
+
for (const path of Array.from(edgePaths)) {
|
|
318
|
+
const rect = path.getBoundingClientRect();
|
|
319
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
320
|
+
const flowX = (rect.left - containerRect.left - vt.x) / vt.zoom;
|
|
321
|
+
const flowY = (rect.top - containerRect.top - vt.y) / vt.zoom;
|
|
322
|
+
const flowRight = (rect.right - containerRect.left - vt.x) / vt.zoom;
|
|
323
|
+
const flowBottom = (rect.bottom - containerRect.top - vt.y) / vt.zoom;
|
|
324
|
+
combined = unionRects(combined, {
|
|
325
|
+
x: flowX,
|
|
326
|
+
y: flowY,
|
|
327
|
+
width: flowRight - flowX,
|
|
328
|
+
height: flowBottom - flowY
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return combined;
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/utils/download.ts
|
|
335
|
+
const { saveAs } = FileSaver;
|
|
336
|
+
const MAX_PX = 2048;
|
|
337
|
+
const EXPORT_PADDING = .05;
|
|
338
|
+
function buildCaptureOptions(imageWidth, imageHeight, x, y, zoom) {
|
|
339
|
+
return {
|
|
340
|
+
backgroundColor: "#ffffff",
|
|
341
|
+
width: imageWidth,
|
|
342
|
+
height: imageHeight,
|
|
343
|
+
skipFonts: true,
|
|
344
|
+
style: {
|
|
345
|
+
width: `${imageWidth}px`,
|
|
346
|
+
height: `${imageHeight}px`,
|
|
347
|
+
transform: `translate(${x}px, ${y}px) scale(${zoom})`
|
|
348
|
+
},
|
|
349
|
+
filter: (node) => !(node instanceof Element) || !node.classList.contains("react-flow__panel") && !node.classList.contains("react-flow__background")
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function resolveViewport(container, nodes, getNodesBounds) {
|
|
353
|
+
if (nodes.length === 0) return null;
|
|
354
|
+
const viewportEl = container.querySelector(".react-flow__viewport");
|
|
355
|
+
if (!viewportEl) return null;
|
|
356
|
+
const bounds = getContentBounds(nodes, viewportEl, getNodesBounds);
|
|
357
|
+
const padX = bounds.width * EXPORT_PADDING;
|
|
358
|
+
const padY = bounds.height * EXPORT_PADDING;
|
|
359
|
+
const paddedWidth = bounds.width + 2 * padX;
|
|
360
|
+
const paddedHeight = bounds.height + 2 * padY;
|
|
361
|
+
const zoom = Math.min(MAX_PX / paddedWidth, MAX_PX / paddedHeight);
|
|
362
|
+
return {
|
|
363
|
+
viewportEl,
|
|
364
|
+
imageWidth: Math.round(paddedWidth * zoom),
|
|
365
|
+
imageHeight: Math.round(paddedHeight * zoom),
|
|
366
|
+
x: (padX - bounds.x) * zoom,
|
|
367
|
+
y: (padY - bounds.y) * zoom,
|
|
368
|
+
zoom
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async function downloadPng(container, nodes, getNodesBounds) {
|
|
372
|
+
const capture = resolveViewport(container, nodes, getNodesBounds);
|
|
373
|
+
if (!capture) return;
|
|
374
|
+
const { viewportEl, imageWidth, imageHeight, x, y, zoom } = capture;
|
|
375
|
+
const dataUrl = await toPng(viewportEl, buildCaptureOptions(imageWidth, imageHeight, x, y, zoom));
|
|
376
|
+
saveAs(dataUrl, "graph.png");
|
|
377
|
+
}
|
|
378
|
+
async function downloadSvg(container, nodes, getNodesBounds) {
|
|
379
|
+
const capture = resolveViewport(container, nodes, getNodesBounds);
|
|
380
|
+
if (!capture) return;
|
|
381
|
+
const { viewportEl, imageWidth, imageHeight, x, y, zoom } = capture;
|
|
382
|
+
const dataUrl = await toSvg(viewportEl, buildCaptureOptions(imageWidth, imageHeight, x, y, zoom));
|
|
383
|
+
saveAs(dataUrl, "graph.svg");
|
|
384
|
+
}
|
|
385
|
+
async function downloadPdf(container, nodes, getNodesBounds) {
|
|
386
|
+
const capture = resolveViewport(container, nodes, getNodesBounds);
|
|
387
|
+
if (!capture) return;
|
|
388
|
+
const { viewportEl, imageWidth, imageHeight, x, y, zoom } = capture;
|
|
389
|
+
const dataUrl = await toPng(viewportEl, buildCaptureOptions(imageWidth, imageHeight, x, y, zoom));
|
|
390
|
+
const pdf = new jsPDF({
|
|
391
|
+
orientation: imageWidth >= imageHeight ? "landscape" : "portrait",
|
|
392
|
+
unit: "px",
|
|
393
|
+
format: [imageWidth, imageHeight]
|
|
394
|
+
});
|
|
395
|
+
pdf.addImage(dataUrl, "PNG", 0, 0, imageWidth, imageHeight);
|
|
396
|
+
pdf.save("graph.pdf");
|
|
397
|
+
}
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/useGraphExport.ts
|
|
400
|
+
const useGraphExport = (wrapperRef) => {
|
|
401
|
+
const { getNodes, getNodesBounds } = useReactFlow();
|
|
402
|
+
return {
|
|
403
|
+
downloadPng: useCallback(() => {
|
|
404
|
+
if (!wrapperRef.current) return;
|
|
405
|
+
downloadPng(wrapperRef.current, getNodes(), getNodesBounds);
|
|
406
|
+
}, [
|
|
407
|
+
wrapperRef,
|
|
408
|
+
getNodes,
|
|
409
|
+
getNodesBounds
|
|
410
|
+
]),
|
|
411
|
+
downloadSvg: useCallback(() => {
|
|
412
|
+
if (!wrapperRef.current) return;
|
|
413
|
+
downloadSvg(wrapperRef.current, getNodes(), getNodesBounds);
|
|
414
|
+
}, [
|
|
415
|
+
wrapperRef,
|
|
416
|
+
getNodes,
|
|
417
|
+
getNodesBounds
|
|
418
|
+
]),
|
|
419
|
+
downloadPdf: useCallback(() => {
|
|
420
|
+
if (!wrapperRef.current) return;
|
|
421
|
+
downloadPdf(wrapperRef.current, getNodes(), getNodesBounds);
|
|
422
|
+
}, [
|
|
423
|
+
wrapperRef,
|
|
424
|
+
getNodes,
|
|
425
|
+
getNodesBounds
|
|
426
|
+
])
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
//#endregion
|
|
159
430
|
//#region src/components/ResourceView/Viewers/GraphFlow/model/types.ts
|
|
160
|
-
var
|
|
431
|
+
var GraphError = class extends Error {
|
|
161
432
|
details;
|
|
162
433
|
constructor(message, details) {
|
|
163
434
|
super(message);
|
|
164
|
-
this.name = "
|
|
435
|
+
this.name = "GraphError";
|
|
165
436
|
this.details = details;
|
|
166
437
|
}
|
|
167
438
|
};
|
|
439
|
+
const toGraphError = (error) => error instanceof GraphError ? error : new GraphError("Graph rendering failed.", [error instanceof Error ? error.message : String(error)]);
|
|
168
440
|
const DEFAULT_COLORS = {
|
|
169
441
|
edge: "#555555",
|
|
170
442
|
place: "#aec6e8",
|
|
@@ -174,7 +446,7 @@ const DEFAULT_COLORS = {
|
|
|
174
446
|
};
|
|
175
447
|
const FIT_VIEW_PADDING = .15;
|
|
176
448
|
//#endregion
|
|
177
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/
|
|
449
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/layout/elk.ts
|
|
178
450
|
const EDGE_ROUTINGS = /* @__PURE__ */ new Set([
|
|
179
451
|
"SPLINES",
|
|
180
452
|
"ORTHOGONAL",
|
|
@@ -182,252 +454,151 @@ const EDGE_ROUTINGS = /* @__PURE__ */ new Set([
|
|
|
182
454
|
]);
|
|
183
455
|
const EDGE_ROUTING_KEYS = ["elk.edgeRouting", "org.eclipse.elk.edgeRouting"];
|
|
184
456
|
const normalizeEdgeRouting = (value) => {
|
|
185
|
-
if (typeof value !== "string") throw new
|
|
457
|
+
if (typeof value !== "string") throw new GraphError("Invalid ELK edge routing.", [`Expected one of ${[...EDGE_ROUTINGS].join(", ")}, got ${String(value)}.`]);
|
|
186
458
|
const normalized = value.toUpperCase();
|
|
187
|
-
if (!EDGE_ROUTINGS.has(normalized)) throw new
|
|
459
|
+
if (!EDGE_ROUTINGS.has(normalized)) throw new GraphError("Invalid ELK edge routing.", [`Unknown edge routing "${value}". Supported values are ${[...EDGE_ROUTINGS].join(", ")}.`]);
|
|
188
460
|
return normalized;
|
|
189
461
|
};
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
for (const key of EDGE_ROUTING_KEYS) if (key in
|
|
462
|
+
const resolveGraphRouting = (options) => {
|
|
463
|
+
const routing = EDGE_ROUTING_KEYS.flatMap((key) => key in options ? [normalizeEdgeRouting(options[key])] : [])[0] ?? "POLYLINE";
|
|
464
|
+
const normalized = { ...options };
|
|
465
|
+
for (const key of EDGE_ROUTING_KEYS) if (key in normalized) normalized[key] = routing;
|
|
194
466
|
return {
|
|
195
|
-
|
|
196
|
-
|
|
467
|
+
routing,
|
|
468
|
+
options: normalized
|
|
197
469
|
};
|
|
198
470
|
};
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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));
|
|
471
|
+
const sectionPoints = (section) => {
|
|
472
|
+
if (!section.startPoint || !section.endPoint) return [];
|
|
473
|
+
return [
|
|
474
|
+
section.startPoint,
|
|
475
|
+
...section.bendPoints ?? [],
|
|
476
|
+
section.endPoint
|
|
477
|
+
];
|
|
253
478
|
};
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
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) => {
|
|
479
|
+
const isSplineControlList = (points) => points.length >= 4 && (points.length - 1) % 3 === 0;
|
|
480
|
+
const routeSection = (section, routing) => {
|
|
481
|
+
const points = sectionPoints(section);
|
|
278
482
|
if (points.length < 2) return null;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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}`;
|
|
483
|
+
if (routing === "SPLINES" && section.startPoint) {
|
|
484
|
+
const control = [section.startPoint, ...section.bendPoints ?? []];
|
|
485
|
+
if ((control.length - 1) % 3 === 2 && section.endPoint) control.push(section.endPoint);
|
|
486
|
+
if (isSplineControlList(control)) return {
|
|
487
|
+
kind: "spline",
|
|
488
|
+
points: control,
|
|
489
|
+
startAnchor: null,
|
|
490
|
+
endAnchor: null
|
|
491
|
+
};
|
|
294
492
|
}
|
|
295
|
-
return
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (!start || !end) return null;
|
|
302
|
-
return buildPolylinePath$1([
|
|
303
|
-
start,
|
|
304
|
-
...section.bendPoints ?? [],
|
|
305
|
-
end
|
|
306
|
-
]);
|
|
493
|
+
return {
|
|
494
|
+
kind: "polyline",
|
|
495
|
+
points,
|
|
496
|
+
startAnchor: null,
|
|
497
|
+
endAnchor: null
|
|
498
|
+
};
|
|
307
499
|
};
|
|
308
500
|
const sectionMidpoint = (section) => {
|
|
309
|
-
|
|
310
|
-
const points = [
|
|
311
|
-
section.startPoint,
|
|
312
|
-
...section.bendPoints ?? [],
|
|
313
|
-
section.endPoint
|
|
314
|
-
];
|
|
501
|
+
const points = sectionPoints(section);
|
|
315
502
|
const mid = points[Math.floor(points.length / 2)];
|
|
316
503
|
return mid ? {
|
|
317
504
|
x: mid.x,
|
|
318
505
|
y: mid.y
|
|
319
506
|
} : null;
|
|
320
507
|
};
|
|
321
|
-
const
|
|
508
|
+
const labelPosition = (edge, section) => {
|
|
509
|
+
const label = edge.labels?.[0];
|
|
322
510
|
if (label?.x != null && label?.y != null) return {
|
|
323
511
|
x: label.x + (label.width ?? 0) / 2,
|
|
324
512
|
y: label.y + (label.height ?? 0) / 2
|
|
325
513
|
};
|
|
326
514
|
return sectionMidpoint(section);
|
|
327
515
|
};
|
|
328
|
-
const
|
|
329
|
-
const getEdgeRouting = (edge, fallback) => {
|
|
516
|
+
const edgeRoutingOf = (edge, fallback) => {
|
|
330
517
|
const options = edge.layoutOptions ?? {};
|
|
331
|
-
const
|
|
332
|
-
return
|
|
518
|
+
const routing = options["elk.edgeRouting"] ?? options["org.eclipse.elk.edgeRouting"];
|
|
519
|
+
return routing === void 0 ? fallback : normalizeEdgeRouting(routing);
|
|
333
520
|
};
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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 {
|
|
521
|
+
const byId = (a, b) => a.id.localeCompare(b.id);
|
|
522
|
+
const byEndpoints = (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.id.localeCompare(b.id);
|
|
523
|
+
const nodeSize = (node) => ({
|
|
524
|
+
width: node.width ?? 120,
|
|
525
|
+
height: node.height ?? 40
|
|
526
|
+
});
|
|
527
|
+
var ElkEngine = class ElkEngine {
|
|
528
|
+
config;
|
|
529
|
+
static elk = new ELK();
|
|
530
|
+
constructor(config) {
|
|
531
|
+
this.config = config;
|
|
532
|
+
}
|
|
533
|
+
async run(model) {
|
|
534
|
+
const { routing, options } = resolveGraphRouting(this.config.options);
|
|
535
|
+
const layoutOptions = model.nodes.some((node) => typeof node.rank === "number") ? {
|
|
536
|
+
"elk.partitioning.activate": true,
|
|
537
|
+
...options
|
|
538
|
+
} : options;
|
|
539
|
+
const graph = await ElkEngine.elk.layout({
|
|
540
|
+
id: "root",
|
|
541
|
+
layoutOptions,
|
|
542
|
+
children: [...model.nodes].sort(byId).map((node) => {
|
|
543
|
+
const { width, height } = nodeSize(node);
|
|
544
|
+
return {
|
|
545
|
+
id: node.id,
|
|
546
|
+
width,
|
|
547
|
+
height,
|
|
548
|
+
layoutOptions: this.nodeLayoutOptions(node)
|
|
549
|
+
};
|
|
550
|
+
}),
|
|
551
|
+
edges: [...model.edges].sort(byEndpoints).map((edge) => ({
|
|
390
552
|
id: edge.id,
|
|
391
553
|
sources: [edge.source],
|
|
392
554
|
targets: [edge.target],
|
|
393
|
-
layoutOptions:
|
|
394
|
-
...
|
|
395
|
-
text: String(
|
|
396
|
-
width: String(
|
|
555
|
+
layoutOptions: edge.layout_attrs ?? {},
|
|
556
|
+
...edge.label ? { labels: [{
|
|
557
|
+
text: String(edge.label),
|
|
558
|
+
width: String(edge.label).length * 7 + 12,
|
|
397
559
|
height: 18
|
|
398
560
|
}] } : {}
|
|
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
|
|
561
|
+
}))
|
|
416
562
|
});
|
|
417
563
|
return {
|
|
418
|
-
|
|
419
|
-
|
|
564
|
+
positions: this.readPositions(graph.children),
|
|
565
|
+
routes: this.readRoutes(graph.edges, routing)
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
nodeLayoutOptions(node) {
|
|
569
|
+
const options = { ...node.layout_attrs ?? {} };
|
|
570
|
+
const rank = node.rank ?? null;
|
|
571
|
+
if (rank === "source") options["elk.layered.layering.layerConstraint"] = "FIRST";
|
|
572
|
+
else if (rank === "sink") options["elk.layered.layering.layerConstraint"] = "LAST";
|
|
573
|
+
else if (typeof rank === "number") options["elk.partitioning.partition"] = String(rank);
|
|
574
|
+
return options;
|
|
575
|
+
}
|
|
576
|
+
readPositions(children = []) {
|
|
577
|
+
const positions = {};
|
|
578
|
+
for (const child of children) if (child.id && child.x != null && child.y != null) positions[child.id] = {
|
|
579
|
+
x: child.x,
|
|
580
|
+
y: child.y
|
|
420
581
|
};
|
|
421
|
-
|
|
582
|
+
return positions;
|
|
583
|
+
}
|
|
584
|
+
readRoutes(edges = [], graphRouting) {
|
|
585
|
+
const routes = {};
|
|
586
|
+
for (const edge of edges) {
|
|
587
|
+
const section = edge.sections?.[0];
|
|
588
|
+
if (!edge.id || !section) continue;
|
|
589
|
+
const routed = routeSection(section, edgeRoutingOf(edge, graphRouting));
|
|
590
|
+
if (!routed) continue;
|
|
591
|
+
routes[edge.id] = {
|
|
592
|
+
...routed,
|
|
593
|
+
labelPosition: labelPosition(edge, section)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return routes;
|
|
597
|
+
}
|
|
422
598
|
};
|
|
423
599
|
//#endregion
|
|
424
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/
|
|
600
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/layout/graphviz.ts
|
|
425
601
|
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
602
|
const parsePoint = (value) => {
|
|
432
603
|
if (!value) return null;
|
|
433
604
|
const [xRaw, yRaw] = value.split(",");
|
|
@@ -439,38 +610,200 @@ const parsePoint = (value) => {
|
|
|
439
610
|
} : null;
|
|
440
611
|
};
|
|
441
612
|
const parseBoundingBoxHeight = (bb) => {
|
|
442
|
-
const height =
|
|
613
|
+
const height = bb?.split(",").map((v) => Number.parseFloat(v))?.[3];
|
|
443
614
|
return Number.isFinite(height) ? height : 0;
|
|
444
615
|
};
|
|
445
|
-
const
|
|
616
|
+
const parseDimension = (value) => {
|
|
446
617
|
if (value == null) return null;
|
|
447
618
|
const dimension = Number.parseFloat(String(value)) * GRAPHVIZ_DPI;
|
|
448
619
|
return Number.isFinite(dimension) ? dimension : null;
|
|
449
620
|
};
|
|
450
|
-
const
|
|
621
|
+
const flipY = (point, graphHeight) => ({
|
|
622
|
+
x: point.x,
|
|
623
|
+
y: graphHeight - point.y
|
|
624
|
+
});
|
|
625
|
+
const parseEdgePosition = (pos) => {
|
|
451
626
|
const result = {
|
|
452
627
|
points: [],
|
|
453
|
-
|
|
454
|
-
|
|
628
|
+
startArrow: null,
|
|
629
|
+
endArrow: null
|
|
455
630
|
};
|
|
456
631
|
if (!pos) return result;
|
|
457
|
-
const
|
|
458
|
-
for (const match of
|
|
632
|
+
const matches = pos.matchAll(/(?:^|\s)(?:(s|e),)?(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g);
|
|
633
|
+
for (const match of matches) {
|
|
459
634
|
const point = {
|
|
460
635
|
x: Number.parseFloat(match[2] ?? "0"),
|
|
461
636
|
y: Number.parseFloat(match[3] ?? "0")
|
|
462
637
|
};
|
|
463
638
|
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) continue;
|
|
464
|
-
if (match[1] === "s") result.
|
|
465
|
-
else if (match[1] === "e") result.
|
|
639
|
+
if (match[1] === "s") result.startArrow = point;
|
|
640
|
+
else if (match[1] === "e") result.endArrow = point;
|
|
466
641
|
else result.points.push(point);
|
|
467
642
|
}
|
|
468
643
|
return result;
|
|
469
644
|
};
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
645
|
+
const edgeEndpointKey = (edge, gvidToName) => {
|
|
646
|
+
const tail = typeof edge.tail === "number" ? gvidToName.get(edge.tail) : edge.tail;
|
|
647
|
+
const head = typeof edge.head === "number" ? gvidToName.get(edge.head) : edge.head;
|
|
648
|
+
return tail && head ? `${tail}->${head}` : null;
|
|
649
|
+
};
|
|
650
|
+
const quote = (value) => `"${value.replaceAll("\"", "\\\"")}"`;
|
|
651
|
+
const attrsToDot = (attrs) => attrs ? Object.entries(attrs).filter(([, value]) => value != null).map(([key, value]) => typeof value === "string" ? `${key}=${quote(value)}` : `${key}=${String(value)}`).join(",") : "";
|
|
652
|
+
const dotShape = (shape) => {
|
|
653
|
+
switch (shape) {
|
|
654
|
+
case "circle":
|
|
655
|
+
case "diamond":
|
|
656
|
+
case "hexagon":
|
|
657
|
+
case "triangle": return shape;
|
|
658
|
+
default: return "box";
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
var GraphvizEngine = class GraphvizEngine {
|
|
662
|
+
config;
|
|
663
|
+
static instance = null;
|
|
664
|
+
constructor(config) {
|
|
665
|
+
this.config = config;
|
|
666
|
+
}
|
|
667
|
+
static load() {
|
|
668
|
+
GraphvizEngine.instance ??= Graphviz.load();
|
|
669
|
+
return GraphvizEngine.instance;
|
|
670
|
+
}
|
|
671
|
+
async run(model) {
|
|
672
|
+
const graphviz = await GraphvizEngine.load();
|
|
673
|
+
const json = JSON.parse(graphviz.layout(this.toDot(model), "json", this.config.engine));
|
|
674
|
+
const graphHeight = parseBoundingBoxHeight(json.bb);
|
|
675
|
+
const gvidToName = /* @__PURE__ */ new Map();
|
|
676
|
+
const positions = {};
|
|
677
|
+
for (const object of json.objects ?? []) {
|
|
678
|
+
if (object._gvid != null && object.name) gvidToName.set(object._gvid, object.name);
|
|
679
|
+
if (!object.name) continue;
|
|
680
|
+
const center = parsePoint(object.pos);
|
|
681
|
+
if (!center) continue;
|
|
682
|
+
const node = model.nodes.find((candidate) => candidate.id === object.name);
|
|
683
|
+
const width = node?.width ?? parseDimension(object.width) ?? 0;
|
|
684
|
+
const height = node?.height ?? parseDimension(object.height) ?? 0;
|
|
685
|
+
const flipped = flipY(center, graphHeight);
|
|
686
|
+
positions[object.name] = {
|
|
687
|
+
x: flipped.x - width / 2,
|
|
688
|
+
y: flipped.y - height / 2
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
positions,
|
|
693
|
+
routes: this.readRoutes(json.edges ?? [], graphHeight, model.edges, gvidToName)
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
readRoutes(graphvizEdges, graphHeight, modelEdges, gvidToName) {
|
|
697
|
+
const idsByKey = modelEdges.reduce((acc, edge) => {
|
|
698
|
+
const key = `${edge.source}->${edge.target}`;
|
|
699
|
+
acc[key] ??= [];
|
|
700
|
+
acc[key].push(edge.id);
|
|
701
|
+
return acc;
|
|
702
|
+
}, {});
|
|
703
|
+
const consumed = /* @__PURE__ */ new Map();
|
|
704
|
+
const routes = {};
|
|
705
|
+
for (const edge of graphvizEdges) {
|
|
706
|
+
const key = edgeEndpointKey(edge, gvidToName);
|
|
707
|
+
const index = key ? consumed.get(key) ?? 0 : 0;
|
|
708
|
+
const id = edge.id ?? (key ? idsByKey[key]?.[index] : void 0);
|
|
709
|
+
if (!id) continue;
|
|
710
|
+
if (key) consumed.set(key, index + 1);
|
|
711
|
+
const position = parseEdgePosition(edge.pos);
|
|
712
|
+
const points = position.points.map((point) => flipY(point, graphHeight));
|
|
713
|
+
const label = parsePoint(edge.lp);
|
|
714
|
+
routes[id] = {
|
|
715
|
+
kind: "spline",
|
|
716
|
+
points,
|
|
717
|
+
startAnchor: position.startArrow ? flipY(position.startArrow, graphHeight) : null,
|
|
718
|
+
endAnchor: position.endArrow ? flipY(position.endArrow, graphHeight) : null,
|
|
719
|
+
labelPosition: label ? flipY(label, graphHeight) : null
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
return routes;
|
|
723
|
+
}
|
|
724
|
+
toDot(model) {
|
|
725
|
+
const { graphAttrs, nodeAttrs, edgeAttrs } = this.config;
|
|
726
|
+
const lines = [
|
|
727
|
+
"digraph G {",
|
|
728
|
+
`graph [${attrsToDot(graphAttrs)}];`,
|
|
729
|
+
`node [${attrsToDot(nodeAttrs)}];`,
|
|
730
|
+
`edge [${attrsToDot(edgeAttrs)}];`
|
|
731
|
+
];
|
|
732
|
+
for (const node of model.nodes) {
|
|
733
|
+
const attrs = {
|
|
734
|
+
...node.layout_attrs,
|
|
735
|
+
label: node.label ?? "",
|
|
736
|
+
shape: dotShape(node.shape),
|
|
737
|
+
color: node.border_color ?? node.color ?? void 0,
|
|
738
|
+
fillcolor: node.color ?? void 0,
|
|
739
|
+
style: node.color ? "filled" : void 0,
|
|
740
|
+
width: node.width ? (node.width / GRAPHVIZ_DPI).toFixed(4) : void 0,
|
|
741
|
+
height: node.height ? (node.height / GRAPHVIZ_DPI).toFixed(4) : void 0,
|
|
742
|
+
fixedsize: node.width && node.height ? true : void 0
|
|
743
|
+
};
|
|
744
|
+
lines.push(`${quote(node.id)} [${attrsToDot(attrs)}];`);
|
|
745
|
+
}
|
|
746
|
+
for (const edge of model.edges) {
|
|
747
|
+
const attrs = {
|
|
748
|
+
...edge.layout_attrs,
|
|
749
|
+
id: edge.id,
|
|
750
|
+
label: edge.label ?? void 0,
|
|
751
|
+
color: edge.color ?? void 0
|
|
752
|
+
};
|
|
753
|
+
lines.push(`${quote(edge.source)} -> ${quote(edge.target)} [${attrsToDot(attrs)}];`);
|
|
754
|
+
}
|
|
755
|
+
lines.push(...this.rankConstraints(model.nodes), "}");
|
|
756
|
+
return lines.join("\n");
|
|
757
|
+
}
|
|
758
|
+
rankConstraints(nodes) {
|
|
759
|
+
const ranks = nodes.filter((node) => node.rank != null).reduce((acc, node) => {
|
|
760
|
+
const key = node.rank;
|
|
761
|
+
acc[key] ??= [];
|
|
762
|
+
acc[key].push(quote(node.id));
|
|
763
|
+
return acc;
|
|
764
|
+
}, {});
|
|
765
|
+
return Object.entries(ranks).map(([rank, ids]) => `{ rank=${["source", "sink"].includes(rank) ? rank : "same"}; ${(ids ?? []).join(" ")} }`);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
//#endregion
|
|
769
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/layout/engine.ts
|
|
770
|
+
const selectEngine = (visualization) => {
|
|
771
|
+
const config = visualization.layout_config;
|
|
772
|
+
switch (config?.type) {
|
|
773
|
+
case "graphviz":
|
|
774
|
+
if (!config.engine) throw new GraphError("Invalid Graphviz layout config.", ["Missing Graphviz layout engine."]);
|
|
775
|
+
return new GraphvizEngine({
|
|
776
|
+
engine: config.engine,
|
|
777
|
+
graphAttrs: config.graphAttrs ?? {},
|
|
778
|
+
nodeAttrs: config.nodeAttrs ?? {},
|
|
779
|
+
edgeAttrs: config.edgeAttrs ?? {}
|
|
780
|
+
});
|
|
781
|
+
case "elk": return new ElkEngine({ options: config.options ?? {} });
|
|
782
|
+
default: throw new GraphError("Invalid graph layout config.", ["Expected an ELK or Graphviz layout config from the backend."]);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/model/buildModel.ts
|
|
787
|
+
const requireNodeId = (node, index) => {
|
|
788
|
+
if (!node.id) throw new GraphError("Invalid graph node.", [`Node at index ${index} is missing an id.`]);
|
|
789
|
+
return {
|
|
790
|
+
...node,
|
|
791
|
+
id: node.id
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
const requireEdgeId = (edge, index) => {
|
|
795
|
+
if (!edge.id) throw new GraphError("Invalid graph edge.", [`Edge at index ${index} is missing an id.`]);
|
|
796
|
+
return {
|
|
797
|
+
...edge,
|
|
798
|
+
id: edge.id
|
|
799
|
+
};
|
|
800
|
+
};
|
|
801
|
+
const buildGraphModel = (visualization) => ({
|
|
802
|
+
nodes: (visualization.nodes ?? []).map(requireNodeId),
|
|
803
|
+
edges: (visualization.edges ?? []).map(requireEdgeId)
|
|
473
804
|
});
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/routeToPath.ts
|
|
474
807
|
const buildPolylinePath = (points) => {
|
|
475
808
|
if (points.length < 2) return null;
|
|
476
809
|
return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x},${point.y}`).join(" ");
|
|
@@ -479,643 +812,168 @@ const buildSplinePath = (points) => {
|
|
|
479
812
|
if (points.length < 4 || (points.length - 1) % 3 !== 0) return null;
|
|
480
813
|
let path = `M ${points[0]?.x},${points[0]?.y}`;
|
|
481
814
|
for (let index = 1; index < points.length; index += 3) {
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
if (!
|
|
486
|
-
path += ` C ${
|
|
815
|
+
const c1 = points[index];
|
|
816
|
+
const c2 = points[index + 1];
|
|
817
|
+
const anchor = points[index + 2];
|
|
818
|
+
if (!c1 || !c2 || !anchor) return null;
|
|
819
|
+
path += ` C ${c1.x},${c1.y} ${c2.x},${c2.y} ${anchor.x},${anchor.y}`;
|
|
487
820
|
}
|
|
488
821
|
return path;
|
|
489
822
|
};
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
823
|
+
const stitchAnchors = (body, points, startAnchor, endAnchor) => [
|
|
824
|
+
startAnchor && points[0] ? `M ${startAnchor.x},${startAnchor.y} L ${points[0].x},${points[0].y}` : null,
|
|
825
|
+
startAnchor ? body.replace(/^M [^C]+/, "") : body,
|
|
826
|
+
endAnchor ? `L ${endAnchor.x},${endAnchor.y}` : null
|
|
827
|
+
].filter(Boolean).join(" ");
|
|
828
|
+
const routeToPath = (route) => {
|
|
829
|
+
const { kind, points, startAnchor, endAnchor } = route;
|
|
830
|
+
if (kind === "spline") {
|
|
831
|
+
const spline = buildSplinePath(points);
|
|
832
|
+
if (spline) return stitchAnchors(spline, points, startAnchor, endAnchor);
|
|
833
|
+
}
|
|
497
834
|
return buildPolylinePath([
|
|
498
|
-
...
|
|
835
|
+
...startAnchor ? [startAnchor] : [],
|
|
499
836
|
...points,
|
|
500
|
-
...
|
|
837
|
+
...endAnchor ? [endAnchor] : []
|
|
501
838
|
]);
|
|
502
839
|
};
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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,
|
|
840
|
+
const midpoint = (a, b) => ({
|
|
841
|
+
x: (a.x + b.x) / 2,
|
|
842
|
+
y: (a.y + b.y) / 2
|
|
843
|
+
});
|
|
844
|
+
const isDegenerate = (a, b) => Math.hypot(a.x - b.x, a.y - b.y) < 1;
|
|
845
|
+
const straightEdgeLayout = (source, target) => ({
|
|
846
|
+
path: `M ${source.x},${source.y} L ${target.x},${target.y}`,
|
|
847
|
+
startPoint: source,
|
|
848
|
+
endPoint: target,
|
|
849
|
+
labelPosition: midpoint(source, target)
|
|
850
|
+
});
|
|
851
|
+
//#endregion
|
|
852
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/project.ts
|
|
853
|
+
const projectToRenderModel = (model, layout) => {
|
|
854
|
+
const centers = nodeCenters(model.nodes, layout.positions);
|
|
855
|
+
return {
|
|
856
|
+
nodes: model.nodes.map((node) => ({
|
|
857
|
+
id: node.id,
|
|
858
|
+
type: "node",
|
|
859
|
+
position: layout.positions[node.id] ?? {
|
|
860
|
+
x: node.x ?? 0,
|
|
861
|
+
y: node.y ?? 0
|
|
862
|
+
},
|
|
863
|
+
data: { model: node }
|
|
864
|
+
})),
|
|
865
|
+
edges: model.edges.map((edge) => ({
|
|
536
866
|
id: edge.id,
|
|
537
|
-
|
|
538
|
-
|
|
867
|
+
source: edge.source,
|
|
868
|
+
target: edge.target,
|
|
869
|
+
type: "graphflow",
|
|
870
|
+
data: {
|
|
871
|
+
model: edge,
|
|
872
|
+
layout: finalizeEdge(layout.routes[edge.id], centers[edge.source], centers[edge.target])
|
|
873
|
+
}
|
|
874
|
+
}))
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
const sizeOf = (node) => ({
|
|
878
|
+
width: node.width ?? 120,
|
|
879
|
+
height: node.height ?? 40
|
|
880
|
+
});
|
|
881
|
+
const nodeCenters = (nodes, positions) => {
|
|
882
|
+
const centers = {};
|
|
883
|
+
for (const node of nodes) {
|
|
884
|
+
const position = positions[node.id];
|
|
885
|
+
if (!position) continue;
|
|
886
|
+
const { width, height } = sizeOf(node);
|
|
887
|
+
centers[node.id] = {
|
|
888
|
+
x: position.x + width / 2,
|
|
889
|
+
y: position.y + height / 2
|
|
539
890
|
};
|
|
540
|
-
lines.push(`${quoteDotValue(edge.source)} -> ${quoteDotValue(edge.target)} [${attrsToDot(attrs)}];`);
|
|
541
891
|
}
|
|
542
|
-
|
|
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;
|
|
892
|
+
return centers;
|
|
556
893
|
};
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
}, {});
|
|
894
|
+
const finalizeEdge = (route, sourceCenter, targetCenter) => {
|
|
895
|
+
if (route) {
|
|
896
|
+
const layout = routeToEdgeLayout(route);
|
|
897
|
+
if (layout) return layout;
|
|
898
|
+
}
|
|
899
|
+
if (sourceCenter && targetCenter && !isDegenerate(sourceCenter, targetCenter)) return straightEdgeLayout(sourceCenter, targetCenter);
|
|
900
|
+
return null;
|
|
591
901
|
};
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
const graphHeight = parseBoundingBoxHeight(json.bb);
|
|
596
|
-
const gvidToName = /* @__PURE__ */ new Map();
|
|
902
|
+
const routeToEdgeLayout = (route) => {
|
|
903
|
+
const path = routeToPath(route);
|
|
904
|
+
if (!path) return null;
|
|
597
905
|
return {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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)
|
|
906
|
+
path,
|
|
907
|
+
startPoint: route.startAnchor ?? route.points[0] ?? null,
|
|
908
|
+
endPoint: route.endAnchor ?? route.points.at(-1) ?? null,
|
|
909
|
+
labelPosition: route.labelPosition
|
|
614
910
|
};
|
|
615
911
|
};
|
|
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
912
|
//#endregion
|
|
629
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/
|
|
630
|
-
const
|
|
913
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/useGraphLayout.ts
|
|
914
|
+
const prepare = (visualization) => {
|
|
631
915
|
try {
|
|
632
916
|
return {
|
|
633
|
-
model:
|
|
634
|
-
|
|
917
|
+
model: buildGraphModel(visualization),
|
|
918
|
+
engine: selectEngine(visualization),
|
|
919
|
+
error: null
|
|
635
920
|
};
|
|
636
921
|
} catch (err) {
|
|
637
922
|
return {
|
|
638
923
|
model: null,
|
|
639
|
-
|
|
924
|
+
engine: null,
|
|
925
|
+
error: toGraphError(err)
|
|
640
926
|
};
|
|
641
927
|
}
|
|
642
928
|
};
|
|
643
|
-
const
|
|
644
|
-
const
|
|
645
|
-
const useLayout = (visualization) => {
|
|
646
|
-
const { model, buildError } = useMemo(() => buildSafely(visualization), [visualization]);
|
|
647
|
-
const runElkLayout = useElkLayout();
|
|
648
|
-
const runGraphvizLayout = useGraphvizLayout(visualization);
|
|
929
|
+
const useGraphLayout = (visualization) => {
|
|
930
|
+
const { model, engine, error: prepareError } = useMemo(() => prepare(visualization), [visualization]);
|
|
649
931
|
const [nodes, setNodes] = useState([]);
|
|
650
932
|
const [edges, setEdges] = useState([]);
|
|
651
|
-
const [
|
|
652
|
-
const [
|
|
933
|
+
const [ready, setReady] = useState(false);
|
|
934
|
+
const [runError, setRunError] = useState(null);
|
|
653
935
|
useEffect(() => {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
936
|
+
setRunError(null);
|
|
937
|
+
if (!model || !engine) {
|
|
938
|
+
setNodes([]);
|
|
939
|
+
setEdges([]);
|
|
940
|
+
setReady(true);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (model.nodes.length === 0) {
|
|
944
|
+
setNodes([]);
|
|
945
|
+
setEdges([]);
|
|
946
|
+
setReady(true);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
setReady(false);
|
|
661
950
|
let cancelled = false;
|
|
662
|
-
(
|
|
663
|
-
if (cancelled
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
951
|
+
engine.run(model).then((result) => {
|
|
952
|
+
if (cancelled) return;
|
|
953
|
+
const projected = projectToRenderModel(model, result);
|
|
954
|
+
setNodes(projected.nodes);
|
|
955
|
+
setEdges(projected.edges);
|
|
956
|
+
setReady(true);
|
|
667
957
|
}, (err) => {
|
|
668
958
|
if (cancelled) return;
|
|
669
|
-
|
|
670
|
-
|
|
959
|
+
setRunError(toGraphError(err));
|
|
960
|
+
setReady(true);
|
|
671
961
|
});
|
|
672
962
|
return () => {
|
|
673
963
|
cancelled = true;
|
|
674
964
|
};
|
|
675
|
-
}, [
|
|
676
|
-
model,
|
|
677
|
-
buildError,
|
|
678
|
-
layoutReady,
|
|
679
|
-
runElkLayout,
|
|
680
|
-
runGraphvizLayout
|
|
681
|
-
]);
|
|
965
|
+
}, [model, engine]);
|
|
682
966
|
const onNodesChange = useCallback((changes) => setNodes((current) => applyNodeChanges(changes, current)), []);
|
|
683
967
|
return {
|
|
684
968
|
nodes,
|
|
685
969
|
edges,
|
|
686
|
-
|
|
687
|
-
error:
|
|
970
|
+
ready,
|
|
971
|
+
error: prepareError ?? runError,
|
|
688
972
|
onNodesChange
|
|
689
973
|
};
|
|
690
974
|
};
|
|
691
975
|
//#endregion
|
|
692
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/render/
|
|
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", {
|
|
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",
|
|
885
|
-
width: "100%",
|
|
886
|
-
height: "100%",
|
|
887
|
-
backgroundColor: color,
|
|
888
|
-
borderRadius: shape === "rectangle" ? 3 : 0,
|
|
889
|
-
clipPath,
|
|
890
|
-
display: "flex",
|
|
891
|
-
alignItems: "center",
|
|
892
|
-
justifyContent: "center",
|
|
893
|
-
overflow: "hidden"
|
|
894
|
-
},
|
|
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
|
-
};
|
|
976
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/edgeArrows.tsx
|
|
1119
977
|
const colorToId = (color) => color.replace(/[^a-zA-Z0-9]/g, "");
|
|
1120
978
|
const ArrowMarker = ({ id, type, color, isStart }) => {
|
|
1121
979
|
if (!type) return null;
|
|
@@ -1282,6 +1140,69 @@ const getMarkerIds = ({ color, startArrow, endArrow }) => {
|
|
|
1282
1140
|
endMarkerId: endArrow ? `arrow-end-${endArrow}-${colorKey}` : null
|
|
1283
1141
|
};
|
|
1284
1142
|
};
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/annotation.tsx
|
|
1145
|
+
const AnnotationBadge = () => /* @__PURE__ */ jsx(Badge, {
|
|
1146
|
+
size: "xs",
|
|
1147
|
+
variant: "filled",
|
|
1148
|
+
color: "gray",
|
|
1149
|
+
title: "This graph element has an annotation visualization.",
|
|
1150
|
+
children: "i"
|
|
1151
|
+
});
|
|
1152
|
+
const NodeAnnotation = ({ annotation }) => {
|
|
1153
|
+
const [opened, setOpened] = useState(false);
|
|
1154
|
+
const toggle = useCallback((e) => {
|
|
1155
|
+
e.stopPropagation();
|
|
1156
|
+
setOpened((o) => !o);
|
|
1157
|
+
}, []);
|
|
1158
|
+
return /* @__PURE__ */ jsxs(Popover, {
|
|
1159
|
+
opened,
|
|
1160
|
+
onClose: () => setOpened(false),
|
|
1161
|
+
position: "right",
|
|
1162
|
+
withArrow: true,
|
|
1163
|
+
withinPortal: true,
|
|
1164
|
+
shadow: "md",
|
|
1165
|
+
children: [/* @__PURE__ */ jsx(Popover.Target, { children: /* @__PURE__ */ jsx("button", {
|
|
1166
|
+
type: "button",
|
|
1167
|
+
className: "nodrag nopan",
|
|
1168
|
+
style: {
|
|
1169
|
+
position: "absolute",
|
|
1170
|
+
right: -8,
|
|
1171
|
+
top: -8,
|
|
1172
|
+
cursor: "pointer",
|
|
1173
|
+
pointerEvents: "all",
|
|
1174
|
+
zIndex: 10,
|
|
1175
|
+
background: "none",
|
|
1176
|
+
border: "none",
|
|
1177
|
+
padding: 0
|
|
1178
|
+
},
|
|
1179
|
+
onClick: toggle,
|
|
1180
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
1181
|
+
children: /* @__PURE__ */ jsx(AnnotationBadge, {})
|
|
1182
|
+
}) }), /* @__PURE__ */ jsxs(Popover.Dropdown, {
|
|
1183
|
+
style: {
|
|
1184
|
+
minWidth: 200,
|
|
1185
|
+
maxWidth: 400
|
|
1186
|
+
},
|
|
1187
|
+
children: [/* @__PURE__ */ jsx(Group, {
|
|
1188
|
+
justify: "flex-end",
|
|
1189
|
+
mb: 4,
|
|
1190
|
+
children: /* @__PURE__ */ jsx(ActionIcon, {
|
|
1191
|
+
size: "xs",
|
|
1192
|
+
variant: "subtle",
|
|
1193
|
+
color: "gray",
|
|
1194
|
+
onClick: () => setOpened(false),
|
|
1195
|
+
children: /* @__PURE__ */ jsx(XIcon, { size: 12 })
|
|
1196
|
+
})
|
|
1197
|
+
}), /* @__PURE__ */ jsx(ScrollArea.Autosize, {
|
|
1198
|
+
mah: 300,
|
|
1199
|
+
children: /* @__PURE__ */ jsx(Visualization, { visualization: annotation })
|
|
1200
|
+
})]
|
|
1201
|
+
})]
|
|
1202
|
+
});
|
|
1203
|
+
};
|
|
1204
|
+
//#endregion
|
|
1205
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/edgeLabels.tsx
|
|
1285
1206
|
const EdgeLabel = ({ color, label, hasAnnotation, x, y }) => /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs("div", {
|
|
1286
1207
|
style: {
|
|
1287
1208
|
position: "absolute",
|
|
@@ -1316,8 +1237,6 @@ const EdgeEndLabel = ({ label, x, y }) => /* @__PURE__ */ jsx(EdgeLabelRenderer,
|
|
|
1316
1237
|
className: "nodrag nopan",
|
|
1317
1238
|
children: label
|
|
1318
1239
|
}) });
|
|
1319
|
-
const END_LABEL_INSET = 22;
|
|
1320
|
-
const END_LABEL_PERP = 9;
|
|
1321
1240
|
const endLabelPositions = (start, end) => {
|
|
1322
1241
|
const dx = end.x - start.x;
|
|
1323
1242
|
const dy = end.y - start.y;
|
|
@@ -1328,40 +1247,82 @@ const endLabelPositions = (start, end) => {
|
|
|
1328
1247
|
const py = ux;
|
|
1329
1248
|
return {
|
|
1330
1249
|
start: {
|
|
1331
|
-
x: start.x + ux *
|
|
1332
|
-
y: start.y + uy *
|
|
1250
|
+
x: start.x + ux * 22 + px * 9,
|
|
1251
|
+
y: start.y + uy * 22 + py * 9
|
|
1333
1252
|
},
|
|
1334
1253
|
end: {
|
|
1335
|
-
x: end.x - ux *
|
|
1336
|
-
y: end.y - uy *
|
|
1254
|
+
x: end.x - ux * 22 + px * 9,
|
|
1255
|
+
y: end.y - uy * 22 + py * 9
|
|
1337
1256
|
}
|
|
1338
1257
|
};
|
|
1339
1258
|
};
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/Edge.tsx
|
|
1261
|
+
const centerOf = ({ sourceX, sourceY, targetX, targetY }) => ({
|
|
1262
|
+
x: (sourceX + targetX) / 2,
|
|
1263
|
+
y: (sourceY + targetY) / 2
|
|
1264
|
+
});
|
|
1265
|
+
const selfLoopPath = (sourceNode) => {
|
|
1266
|
+
const width = sourceNode.measured.width ?? 60;
|
|
1267
|
+
const height = sourceNode.measured.height ?? 34;
|
|
1268
|
+
const position = sourceNode.internals.positionAbsolute;
|
|
1269
|
+
const [path, labelX, labelY] = getSmoothStepPath({
|
|
1270
|
+
sourceX: position.x + width,
|
|
1271
|
+
sourceY: position.y + height * .25,
|
|
1272
|
+
sourcePosition: "right",
|
|
1273
|
+
targetX: position.x + width,
|
|
1274
|
+
targetY: position.y + height * .75,
|
|
1275
|
+
targetPosition: "right",
|
|
1276
|
+
borderRadius: 20,
|
|
1277
|
+
offset: 50
|
|
1278
|
+
});
|
|
1279
|
+
return {
|
|
1280
|
+
path,
|
|
1281
|
+
labelX,
|
|
1282
|
+
labelY
|
|
1283
|
+
};
|
|
1284
|
+
};
|
|
1285
|
+
const bezierFallback = (edge) => {
|
|
1286
|
+
const [path, labelX, labelY] = getBezierPath(edge);
|
|
1287
|
+
return {
|
|
1288
|
+
path,
|
|
1289
|
+
labelX,
|
|
1290
|
+
labelY
|
|
1291
|
+
};
|
|
1292
|
+
};
|
|
1293
|
+
const resolvePath = (edge, layout, sourceNode) => {
|
|
1294
|
+
if (layout?.path) {
|
|
1295
|
+
const center = centerOf(edge);
|
|
1296
|
+
return {
|
|
1297
|
+
path: layout.path,
|
|
1298
|
+
labelX: layout.labelPosition?.x ?? center.x,
|
|
1299
|
+
labelY: layout.labelPosition?.y ?? center.y
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
if (edge.source === edge.target && sourceNode) return selfLoopPath(sourceNode);
|
|
1303
|
+
return bezierFallback(edge);
|
|
1304
|
+
};
|
|
1305
|
+
const GraphFlowEdge = memo((props) => {
|
|
1306
|
+
const { id, source, data, sourceX, sourceY, targetX, targetY } = props;
|
|
1307
|
+
const sourceNode = useInternalNode(source);
|
|
1344
1308
|
if (!data) return null;
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
1347
|
-
const
|
|
1348
|
-
const
|
|
1349
|
-
const
|
|
1309
|
+
const { model, layout } = data;
|
|
1310
|
+
const color = model.color ?? "#555";
|
|
1311
|
+
const dashed = model.style?.dashed ?? false;
|
|
1312
|
+
const bold = model.style?.bold ?? false;
|
|
1313
|
+
const startArrow = model.start_arrow ?? null;
|
|
1314
|
+
const endArrow = model.end_arrow ?? null;
|
|
1350
1315
|
const { startMarkerId, endMarkerId } = getMarkerIds({
|
|
1351
1316
|
color,
|
|
1352
1317
|
startArrow,
|
|
1353
1318
|
endArrow
|
|
1354
1319
|
});
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
sourceNode,
|
|
1358
|
-
targetNode
|
|
1359
|
-
});
|
|
1360
|
-
const startPos = data.startPoint ?? {
|
|
1320
|
+
const resolved = resolvePath(props, layout, sourceNode);
|
|
1321
|
+
const startPos = layout?.startPoint ?? {
|
|
1361
1322
|
x: sourceX,
|
|
1362
1323
|
y: sourceY
|
|
1363
1324
|
};
|
|
1364
|
-
const endPos =
|
|
1325
|
+
const endPos = layout?.endPoint ?? {
|
|
1365
1326
|
x: targetX,
|
|
1366
1327
|
y: targetY
|
|
1367
1328
|
};
|
|
@@ -1379,7 +1340,7 @@ const GraphFlowEdge = memo((props) => {
|
|
|
1379
1340
|
})] }),
|
|
1380
1341
|
/* @__PURE__ */ jsx(BaseEdge, {
|
|
1381
1342
|
id,
|
|
1382
|
-
path:
|
|
1343
|
+
path: resolved.path,
|
|
1383
1344
|
...endMarkerId ? { markerEnd: `url(#${endMarkerId})` } : {},
|
|
1384
1345
|
...startMarkerId ? { markerStart: `url(#${startMarkerId})` } : {},
|
|
1385
1346
|
style: {
|
|
@@ -1388,21 +1349,21 @@ const GraphFlowEdge = memo((props) => {
|
|
|
1388
1349
|
strokeDasharray: dashed ? "6 3" : void 0
|
|
1389
1350
|
}
|
|
1390
1351
|
}),
|
|
1391
|
-
(
|
|
1352
|
+
(model.label || model.annotation) && /* @__PURE__ */ jsx(EdgeLabel, {
|
|
1392
1353
|
color,
|
|
1393
|
-
label:
|
|
1394
|
-
hasAnnotation: Boolean(
|
|
1395
|
-
x:
|
|
1396
|
-
y:
|
|
1354
|
+
label: model.label,
|
|
1355
|
+
hasAnnotation: Boolean(model.annotation),
|
|
1356
|
+
x: resolved.labelX,
|
|
1357
|
+
y: resolved.labelY
|
|
1397
1358
|
}),
|
|
1398
|
-
(
|
|
1359
|
+
(model.start_label || model.end_label) && (() => {
|
|
1399
1360
|
const positions = endLabelPositions(startPos, endPos);
|
|
1400
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1401
|
-
label:
|
|
1361
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [model.start_label && /* @__PURE__ */ jsx(EdgeEndLabel, {
|
|
1362
|
+
label: model.start_label,
|
|
1402
1363
|
x: positions.start.x,
|
|
1403
1364
|
y: positions.start.y
|
|
1404
|
-
}),
|
|
1405
|
-
label:
|
|
1365
|
+
}), model.end_label && /* @__PURE__ */ jsx(EdgeEndLabel, {
|
|
1366
|
+
label: model.end_label,
|
|
1406
1367
|
x: positions.end.x,
|
|
1407
1368
|
y: positions.end.y
|
|
1408
1369
|
})] });
|
|
@@ -1411,201 +1372,242 @@ const GraphFlowEdge = memo((props) => {
|
|
|
1411
1372
|
});
|
|
1412
1373
|
GraphFlowEdge.displayName = "GraphFlowEdge";
|
|
1413
1374
|
//#endregion
|
|
1414
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/
|
|
1415
|
-
|
|
1416
|
-
if (
|
|
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,
|
|
1375
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/nodeShapes.tsx
|
|
1376
|
+
const InnerSymbol = ({ symbol, color }) => {
|
|
1377
|
+
if (symbol === "triangle") return /* @__PURE__ */ jsx("div", { style: {
|
|
1441
1378
|
width: 0,
|
|
1442
|
-
height: 0
|
|
1443
|
-
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1379
|
+
height: 0,
|
|
1380
|
+
borderTop: `${12 * .6}px solid transparent`,
|
|
1381
|
+
borderBottom: `${12 * .6}px solid transparent`,
|
|
1382
|
+
borderLeft: `12px solid ${color}`,
|
|
1383
|
+
marginLeft: 2
|
|
1384
|
+
} });
|
|
1385
|
+
return /* @__PURE__ */ jsx("div", { style: {
|
|
1386
|
+
width: 12,
|
|
1387
|
+
height: 12,
|
|
1388
|
+
backgroundColor: color
|
|
1389
|
+
} });
|
|
1390
|
+
};
|
|
1391
|
+
const CLIP_PATH = {
|
|
1392
|
+
triangle: "polygon(50% 0, 100% 100%, 0 100%)",
|
|
1393
|
+
diamond: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)",
|
|
1394
|
+
hexagon: "polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%)"
|
|
1395
|
+
};
|
|
1396
|
+
const CircleShape = ({ width, height, color, borderColor, doubleBorder, innerSymbol, initialTokens, finalTokens, centerLabel }) => /* @__PURE__ */ jsxs("div", {
|
|
1397
|
+
style: {
|
|
1398
|
+
position: "relative",
|
|
1399
|
+
boxSizing: "border-box",
|
|
1400
|
+
width,
|
|
1401
|
+
height,
|
|
1402
|
+
borderRadius: "50%",
|
|
1403
|
+
backgroundColor: color,
|
|
1404
|
+
border: borderColor ? `2px solid ${borderColor}` : "none",
|
|
1405
|
+
display: "flex",
|
|
1406
|
+
flexDirection: "column",
|
|
1407
|
+
alignItems: "center",
|
|
1408
|
+
justifyContent: "center",
|
|
1409
|
+
gap: 2
|
|
1410
|
+
},
|
|
1411
|
+
children: [
|
|
1412
|
+
doubleBorder && /* @__PURE__ */ jsx("div", { style: {
|
|
1413
|
+
position: "absolute",
|
|
1414
|
+
inset: 5,
|
|
1415
|
+
borderRadius: "50%",
|
|
1416
|
+
border: borderColor ? `2px solid ${borderColor}` : "none",
|
|
1417
|
+
pointerEvents: "none"
|
|
1418
|
+
} }),
|
|
1419
|
+
initialTokens && !innerSymbol && (initialTokens === 1 ? /* @__PURE__ */ jsx("div", { style: {
|
|
1420
|
+
width: 12,
|
|
1421
|
+
height: 12,
|
|
1422
|
+
borderRadius: "50%",
|
|
1423
|
+
backgroundColor: "rgba(0,0,0,0.65)"
|
|
1424
|
+
} }) : /* @__PURE__ */ jsx("span", {
|
|
1425
|
+
style: {
|
|
1426
|
+
color: "#111",
|
|
1427
|
+
fontSize: 11,
|
|
1428
|
+
fontWeight: 700,
|
|
1429
|
+
lineHeight: 1
|
|
1430
|
+
},
|
|
1431
|
+
children: initialTokens
|
|
1432
|
+
})),
|
|
1433
|
+
finalTokens && finalTokens > 1 && /* @__PURE__ */ jsx("span", {
|
|
1434
|
+
style: {
|
|
1435
|
+
position: "absolute",
|
|
1436
|
+
right: 4,
|
|
1437
|
+
bottom: 3,
|
|
1438
|
+
color: "#111",
|
|
1439
|
+
fontSize: 9,
|
|
1440
|
+
fontWeight: 700,
|
|
1441
|
+
lineHeight: 1
|
|
1442
|
+
},
|
|
1443
|
+
children: finalTokens
|
|
1444
|
+
}),
|
|
1445
|
+
innerSymbol && /* @__PURE__ */ jsx(InnerSymbol, {
|
|
1446
|
+
symbol: innerSymbol,
|
|
1447
|
+
color: borderColor ?? "#111"
|
|
1448
|
+
}),
|
|
1449
|
+
centerLabel && /* @__PURE__ */ jsx("span", {
|
|
1450
|
+
style: {
|
|
1451
|
+
maxWidth: width - 6,
|
|
1452
|
+
color: "#111",
|
|
1453
|
+
fontSize: 10,
|
|
1454
|
+
fontWeight: 600,
|
|
1455
|
+
lineHeight: 1,
|
|
1456
|
+
overflow: "hidden",
|
|
1457
|
+
textAlign: "center",
|
|
1458
|
+
textOverflow: "ellipsis",
|
|
1459
|
+
whiteSpace: "nowrap"
|
|
1460
|
+
},
|
|
1461
|
+
children: centerLabel
|
|
1462
|
+
})
|
|
1463
|
+
]
|
|
1464
|
+
});
|
|
1465
|
+
const BoxShape = ({ shape, color, borderColor, doubleBorder, innerSymbol, width, height, children }) => {
|
|
1466
|
+
const clipPath = CLIP_PATH[shape];
|
|
1467
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1468
|
+
style: {
|
|
1469
|
+
position: "relative",
|
|
1470
|
+
boxSizing: "border-box",
|
|
1471
|
+
width: width ?? void 0,
|
|
1472
|
+
height: height ?? void 0,
|
|
1473
|
+
padding: borderColor != null ? 2 : 0,
|
|
1474
|
+
backgroundColor: borderColor ?? "transparent",
|
|
1475
|
+
borderRadius: shape === "rectangle" ? 5 : 0,
|
|
1476
|
+
clipPath
|
|
1477
|
+
},
|
|
1478
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1479
|
+
style: {
|
|
1480
|
+
boxSizing: "border-box",
|
|
1481
|
+
width: "100%",
|
|
1482
|
+
height: "100%",
|
|
1483
|
+
backgroundColor: color,
|
|
1484
|
+
borderRadius: shape === "rectangle" ? 3 : 0,
|
|
1485
|
+
clipPath,
|
|
1486
|
+
display: "flex",
|
|
1487
|
+
alignItems: "center",
|
|
1488
|
+
justifyContent: "center",
|
|
1489
|
+
overflow: "hidden"
|
|
1490
|
+
},
|
|
1491
|
+
children: innerSymbol && !children ? /* @__PURE__ */ jsx(InnerSymbol, {
|
|
1492
|
+
symbol: innerSymbol,
|
|
1493
|
+
color: "#111"
|
|
1494
|
+
}) : children
|
|
1495
|
+
}), doubleBorder && /* @__PURE__ */ jsx("div", { style: {
|
|
1496
|
+
position: "absolute",
|
|
1497
|
+
inset: 6,
|
|
1498
|
+
border: borderColor ? `1.5px solid ${borderColor}` : "none",
|
|
1499
|
+
borderRadius: shape === "rectangle" ? 3 : 0,
|
|
1500
|
+
clipPath,
|
|
1501
|
+
pointerEvents: "none"
|
|
1502
|
+
} })]
|
|
1503
|
+
});
|
|
1504
|
+
};
|
|
1539
1505
|
//#endregion
|
|
1540
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1506
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/Node.tsx
|
|
1507
|
+
const HIDDEN_HANDLE_STYLE = {
|
|
1508
|
+
opacity: 0,
|
|
1509
|
+
pointerEvents: "none"
|
|
1510
|
+
};
|
|
1511
|
+
const HiddenHandles = () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Handle, {
|
|
1512
|
+
type: "target",
|
|
1513
|
+
position: Position.Left,
|
|
1514
|
+
style: HIDDEN_HANDLE_STYLE
|
|
1515
|
+
}), /* @__PURE__ */ jsx(Handle, {
|
|
1516
|
+
type: "source",
|
|
1517
|
+
position: Position.Right,
|
|
1518
|
+
style: HIDDEN_HANDLE_STYLE
|
|
1519
|
+
})] });
|
|
1520
|
+
const ExternalLabel = ({ children, placement }) => /* @__PURE__ */ jsx(Text, {
|
|
1521
|
+
size: "xs",
|
|
1522
|
+
fw: 600,
|
|
1523
|
+
style: {
|
|
1524
|
+
position: "absolute",
|
|
1525
|
+
[placement === "top" ? "bottom" : "top"]: "100%",
|
|
1526
|
+
left: "50%",
|
|
1527
|
+
transform: `translate(-50%, ${placement === "top" ? "-4px" : "4px"})`,
|
|
1528
|
+
whiteSpace: "nowrap",
|
|
1529
|
+
color: DEFAULT_COLORS.text,
|
|
1530
|
+
pointerEvents: "none",
|
|
1531
|
+
letterSpacing: "0.01em"
|
|
1532
|
+
},
|
|
1533
|
+
children
|
|
1534
|
+
});
|
|
1535
|
+
const GraphFlowNode = memo(({ data }) => {
|
|
1536
|
+
const { shape, label, color, border_color, annotation, label_pos, width, height, style } = data.model;
|
|
1537
|
+
const doubleBorder = style?.double_border ?? null;
|
|
1538
|
+
const innerSymbol = style?.inner_symbol ?? null;
|
|
1539
|
+
const initialTokens = style?.initial_tokens ?? null;
|
|
1540
|
+
const finalTokens = style?.final_tokens ?? null;
|
|
1541
|
+
const isExternalLabel = Boolean(label && label_pos !== "center" && label_pos != null);
|
|
1542
|
+
const maxLabelLines = height != null ? Math.max(1, Math.floor((height - 4) / (14 * 1.2))) : void 0;
|
|
1543
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1549
1544
|
style: {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
transform: `translate(${x}px, ${y}px) scale(${zoom})`
|
|
1545
|
+
position: "relative",
|
|
1546
|
+
width: shape === "circle" ? width ?? void 0 : void 0
|
|
1553
1547
|
},
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1548
|
+
children: [
|
|
1549
|
+
/* @__PURE__ */ jsx(HiddenHandles, {}),
|
|
1550
|
+
isExternalLabel && label_pos === "top" && /* @__PURE__ */ jsx(ExternalLabel, {
|
|
1551
|
+
placement: "top",
|
|
1552
|
+
children: label
|
|
1553
|
+
}),
|
|
1554
|
+
shape === "circle" ? /* @__PURE__ */ jsx(CircleShape, {
|
|
1555
|
+
width,
|
|
1556
|
+
height,
|
|
1557
|
+
color: color ?? DEFAULT_COLORS.transition,
|
|
1558
|
+
borderColor: border_color ?? null,
|
|
1559
|
+
doubleBorder,
|
|
1560
|
+
innerSymbol,
|
|
1561
|
+
initialTokens,
|
|
1562
|
+
finalTokens,
|
|
1563
|
+
centerLabel: label_pos === "center" ? label ?? null : null
|
|
1564
|
+
}) : /* @__PURE__ */ jsx(BoxShape, {
|
|
1565
|
+
shape,
|
|
1566
|
+
color: color ?? DEFAULT_COLORS.transition,
|
|
1567
|
+
borderColor: border_color ?? null,
|
|
1568
|
+
doubleBorder,
|
|
1569
|
+
innerSymbol,
|
|
1570
|
+
width: width ?? null,
|
|
1571
|
+
height: height ?? null,
|
|
1572
|
+
children: !isExternalLabel && label && /* @__PURE__ */ jsx(Text, {
|
|
1573
|
+
size: "sm",
|
|
1574
|
+
fw: 500,
|
|
1575
|
+
ta: "center",
|
|
1576
|
+
px: 12,
|
|
1577
|
+
style: {
|
|
1578
|
+
lineHeight: 1.2,
|
|
1579
|
+
color: "#111",
|
|
1580
|
+
maxWidth: width != null ? width : "100%",
|
|
1581
|
+
boxSizing: "border-box",
|
|
1582
|
+
overflow: "hidden",
|
|
1583
|
+
overflowWrap: "anywhere",
|
|
1584
|
+
...maxLabelLines != null ? {
|
|
1585
|
+
display: "-webkit-box",
|
|
1586
|
+
WebkitBoxOrient: "vertical",
|
|
1587
|
+
WebkitLineClamp: maxLabelLines
|
|
1588
|
+
} : { whiteSpace: "normal" }
|
|
1589
|
+
},
|
|
1590
|
+
children: label
|
|
1591
|
+
})
|
|
1592
|
+
}),
|
|
1593
|
+
isExternalLabel && label_pos === "bottom" && /* @__PURE__ */ jsx(ExternalLabel, {
|
|
1594
|
+
placement: "bottom",
|
|
1595
|
+
children: label
|
|
1596
|
+
}),
|
|
1597
|
+
annotation && /* @__PURE__ */ jsx(NodeAnnotation, { annotation })
|
|
1598
|
+
]
|
|
1597
1599
|
});
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
}
|
|
1600
|
+
});
|
|
1601
|
+
GraphFlowNode.displayName = "GraphFlowNode";
|
|
1601
1602
|
//#endregion
|
|
1602
1603
|
//#region src/components/ResourceView/Viewers/GraphFlow/index.tsx
|
|
1603
1604
|
const nodeTypes = { node: GraphFlowNode };
|
|
1604
1605
|
const edgeTypes = { graphflow: GraphFlowEdge };
|
|
1605
1606
|
const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
1606
|
-
const { nodes, edges,
|
|
1607
|
-
const { fitBounds, getNodes, getEdges
|
|
1607
|
+
const { nodes, edges, ready, error, onNodesChange } = useGraphLayout(visualization);
|
|
1608
|
+
const { fitBounds, getNodes, getEdges } = useReactFlow();
|
|
1608
1609
|
const wrapperRef = useRef(null);
|
|
1610
|
+
const { downloadPng, downloadSvg, downloadPdf } = useGraphExport(wrapperRef);
|
|
1609
1611
|
const fitViewWithEdges = useCallback(() => {
|
|
1610
1612
|
fitBounds(computeAutoFitBounds(getNodes(), getEdges()), { padding: FIT_VIEW_PADDING });
|
|
1611
1613
|
}, [
|
|
@@ -1613,19 +1615,16 @@ const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
|
1613
1615
|
getNodes,
|
|
1614
1616
|
getEdges
|
|
1615
1617
|
]);
|
|
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
1618
|
const wasReadyRef = useRef(false);
|
|
1620
1619
|
useEffect(() => {
|
|
1621
|
-
if (!
|
|
1620
|
+
if (!ready) {
|
|
1622
1621
|
wasReadyRef.current = false;
|
|
1623
1622
|
return;
|
|
1624
1623
|
}
|
|
1625
1624
|
if (wasReadyRef.current) return;
|
|
1626
1625
|
wasReadyRef.current = true;
|
|
1627
1626
|
fitViewWithEdges();
|
|
1628
|
-
}, [
|
|
1627
|
+
}, [ready, fitViewWithEdges]);
|
|
1629
1628
|
if (error) return /* @__PURE__ */ jsx(Stack, {
|
|
1630
1629
|
h: "100%",
|
|
1631
1630
|
justify: "center",
|
|
@@ -1641,16 +1640,16 @@ const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
|
1641
1640
|
})
|
|
1642
1641
|
});
|
|
1643
1642
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LoadingOverlay, {
|
|
1644
|
-
visible: !
|
|
1643
|
+
visible: !ready,
|
|
1645
1644
|
zIndex: 10
|
|
1646
1645
|
}), /* @__PURE__ */ jsx("div", {
|
|
1647
1646
|
ref: wrapperRef,
|
|
1648
1647
|
style: {
|
|
1649
1648
|
width: "100%",
|
|
1650
1649
|
height: "100%",
|
|
1651
|
-
opacity:
|
|
1650
|
+
opacity: ready ? 1 : 0
|
|
1652
1651
|
},
|
|
1653
|
-
children: /* @__PURE__ */
|
|
1652
|
+
children: /* @__PURE__ */ jsxs(ReactFlow, {
|
|
1654
1653
|
nodes,
|
|
1655
1654
|
edges,
|
|
1656
1655
|
nodeTypes,
|
|
@@ -1663,50 +1662,18 @@ const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
|
1663
1662
|
panOnDrag: !isPreview,
|
|
1664
1663
|
minZoom: .05,
|
|
1665
1664
|
maxZoom: 3,
|
|
1666
|
-
children: !isPreview && /* @__PURE__ */
|
|
1665
|
+
children: [!isPreview && /* @__PURE__ */ jsx(Background, {
|
|
1667
1666
|
color: "#e5e7eb",
|
|
1668
1667
|
gap: 20,
|
|
1669
1668
|
size: 1
|
|
1670
|
-
}), /* @__PURE__ */
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
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
|
-
})] })
|
|
1669
|
+
}), /* @__PURE__ */ jsx(GraphControls, {
|
|
1670
|
+
isPreview,
|
|
1671
|
+
onFitView: fitViewWithEdges,
|
|
1672
|
+
onDownloadPng: downloadPng,
|
|
1673
|
+
onDownloadSvg: downloadSvg,
|
|
1674
|
+
onDownloadPdf: downloadPdf,
|
|
1675
|
+
menuItems
|
|
1676
|
+
})]
|
|
1710
1677
|
})
|
|
1711
1678
|
})] });
|
|
1712
1679
|
};
|