@ocelescope/resources 0.1.3 → 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 +1055 -1091
- package/package.json +13 -15
package/dist/index.js
CHANGED
|
@@ -6,10 +6,10 @@ import FileSaver from "file-saver";
|
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
|
9
|
-
import { Background, BaseEdge, ControlButton, Controls, EdgeLabelRenderer, Handle, Position, ReactFlow, ReactFlowProvider, applyNodeChanges, getBezierPath, getSmoothStepPath, useInternalNode,
|
|
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";
|
|
@@ -70,7 +70,8 @@ const SvgViewer = ({ visualization, isPreview }) => {
|
|
|
70
70
|
const cleanedSvg = visualization.svg.replace(/width="[^"]+"/, "width=\"100%\"").replace(/height="[^"]+"/, "height=\"100%\"").replace(/<svg(?![^>]*xmlns=)/, "<svg xmlns=\"http://www.w3.org/2000/svg\"");
|
|
71
71
|
const handleDownload = () => {
|
|
72
72
|
if (!cleanedSvg) return;
|
|
73
|
-
|
|
73
|
+
const blob = new Blob([cleanedSvg], { type: "image/svg+xml;charset=utf-8" });
|
|
74
|
+
saveAs$2(blob, "resource.svg");
|
|
74
75
|
};
|
|
75
76
|
const content = /* @__PURE__ */ jsx("div", {
|
|
76
77
|
style: {
|
|
@@ -116,7 +117,8 @@ const DotToSvgViewer = ({ isPreview, visualization }) => {
|
|
|
116
117
|
}, [svg]);
|
|
117
118
|
const handleDownload = () => {
|
|
118
119
|
if (!cleanedSvg) return;
|
|
119
|
-
|
|
120
|
+
const blob = new Blob([cleanedSvg], { type: "image/svg+xml;charset=utf-8" });
|
|
121
|
+
saveAs$1(blob, "resource.svg");
|
|
120
122
|
};
|
|
121
123
|
const content = /* @__PURE__ */ jsxs("div", {
|
|
122
124
|
style: {
|
|
@@ -158,15 +160,283 @@ const DotToSvgViewer = ({ isPreview, visualization }) => {
|
|
|
158
160
|
});
|
|
159
161
|
};
|
|
160
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
|
|
161
430
|
//#region src/components/ResourceView/Viewers/GraphFlow/model/types.ts
|
|
162
|
-
var
|
|
431
|
+
var GraphError = class extends Error {
|
|
163
432
|
details;
|
|
164
433
|
constructor(message, details) {
|
|
165
434
|
super(message);
|
|
166
|
-
this.name = "
|
|
435
|
+
this.name = "GraphError";
|
|
167
436
|
this.details = details;
|
|
168
437
|
}
|
|
169
438
|
};
|
|
439
|
+
const toGraphError = (error) => error instanceof GraphError ? error : new GraphError("Graph rendering failed.", [error instanceof Error ? error.message : String(error)]);
|
|
170
440
|
const DEFAULT_COLORS = {
|
|
171
441
|
edge: "#555555",
|
|
172
442
|
place: "#aec6e8",
|
|
@@ -176,7 +446,7 @@ const DEFAULT_COLORS = {
|
|
|
176
446
|
};
|
|
177
447
|
const FIT_VIEW_PADDING = .15;
|
|
178
448
|
//#endregion
|
|
179
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/
|
|
449
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/layout/elk.ts
|
|
180
450
|
const EDGE_ROUTINGS = /* @__PURE__ */ new Set([
|
|
181
451
|
"SPLINES",
|
|
182
452
|
"ORTHOGONAL",
|
|
@@ -184,252 +454,151 @@ const EDGE_ROUTINGS = /* @__PURE__ */ new Set([
|
|
|
184
454
|
]);
|
|
185
455
|
const EDGE_ROUTING_KEYS = ["elk.edgeRouting", "org.eclipse.elk.edgeRouting"];
|
|
186
456
|
const normalizeEdgeRouting = (value) => {
|
|
187
|
-
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)}.`]);
|
|
188
458
|
const normalized = value.toUpperCase();
|
|
189
|
-
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(", ")}.`]);
|
|
190
460
|
return normalized;
|
|
191
461
|
};
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
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;
|
|
196
466
|
return {
|
|
197
|
-
|
|
198
|
-
|
|
467
|
+
routing,
|
|
468
|
+
options: normalized
|
|
199
469
|
};
|
|
200
470
|
};
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
data: node
|
|
209
|
-
});
|
|
210
|
-
const mapEdge = (edge) => ({
|
|
211
|
-
id: edge.id,
|
|
212
|
-
source: edge.source,
|
|
213
|
-
target: edge.target,
|
|
214
|
-
type: "graphflow",
|
|
215
|
-
data: edge
|
|
216
|
-
});
|
|
217
|
-
const createLayoutPlan = (visualization) => {
|
|
218
|
-
const layoutConfig = visualization.layout_config;
|
|
219
|
-
switch (layoutConfig?.type) {
|
|
220
|
-
case "graphviz":
|
|
221
|
-
if (!layoutConfig.engine) throw new GraphVisualizationError("Invalid Graphviz layout config.", ["Missing Graphviz layout engine."]);
|
|
222
|
-
return {
|
|
223
|
-
type: "graphviz",
|
|
224
|
-
engine: layoutConfig.engine,
|
|
225
|
-
graphAttrs: layoutConfig.graphAttrs ?? {},
|
|
226
|
-
nodeAttrs: layoutConfig.nodeAttrs ?? {},
|
|
227
|
-
edgeAttrs: layoutConfig.edgeAttrs ?? {}
|
|
228
|
-
};
|
|
229
|
-
case "elk": {
|
|
230
|
-
const normalized = normalizeElkOptions(layoutConfig.options ?? {});
|
|
231
|
-
return {
|
|
232
|
-
type: "elk",
|
|
233
|
-
elkOptions: normalized.elkOptions,
|
|
234
|
-
edgeRouting: normalized.edgeRouting
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
default: throw new GraphVisualizationError("Invalid graph layout config.", ["Expected an ELK or Graphviz layout config from the backend."]);
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
const buildGraphFlowModel = (visualization) => ({
|
|
241
|
-
nodes: (visualization.nodes ?? []).map(mapNode),
|
|
242
|
-
edges: (visualization.edges ?? []).map(mapEdge),
|
|
243
|
-
layoutPlan: createLayoutPlan(visualization)
|
|
244
|
-
});
|
|
245
|
-
//#endregion
|
|
246
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/model/runLayout.ts
|
|
247
|
-
const toGraphError = (error) => {
|
|
248
|
-
if (error instanceof GraphVisualizationError) return error;
|
|
249
|
-
return new GraphVisualizationError("Graph layout failed.", [error instanceof Error ? error.message : String(error)]);
|
|
250
|
-
};
|
|
251
|
-
const measuredNodesMatchModel = (measuredNodes, model) => {
|
|
252
|
-
if (measuredNodes.length !== model.nodes.length) return false;
|
|
253
|
-
const measuredNodeIds = new Set(measuredNodes.map((node) => node.id));
|
|
254
|
-
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
|
+
];
|
|
255
478
|
};
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
...node,
|
|
260
|
-
position
|
|
261
|
-
} : node;
|
|
262
|
-
});
|
|
263
|
-
const applyEdgeLayouts = (edges, edgeLayouts) => edges.map((edge) => {
|
|
264
|
-
const layout = edgeLayouts[edge.id];
|
|
265
|
-
if (!layout) return edge;
|
|
266
|
-
return {
|
|
267
|
-
...edge,
|
|
268
|
-
data: {
|
|
269
|
-
...edge.data,
|
|
270
|
-
path: layout.path,
|
|
271
|
-
labelPosition: layout.labelPosition,
|
|
272
|
-
startPoint: layout.startPoint,
|
|
273
|
-
endPoint: layout.endPoint
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
});
|
|
277
|
-
//#endregion
|
|
278
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/useElkLayout.ts
|
|
279
|
-
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);
|
|
280
482
|
if (points.length < 2) return null;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
for (let index = 0; index < bendPoints.length; index += 3) {
|
|
291
|
-
const cp1 = bendPoints[index];
|
|
292
|
-
const cp2 = bendPoints[index + 1];
|
|
293
|
-
if (!cp1 || !cp2) return null;
|
|
294
|
-
const anchor = bendPoints[index + 2] ?? end;
|
|
295
|
-
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
|
+
};
|
|
296
492
|
}
|
|
297
|
-
return
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (!start || !end) return null;
|
|
304
|
-
return buildPolylinePath$1([
|
|
305
|
-
start,
|
|
306
|
-
...section.bendPoints ?? [],
|
|
307
|
-
end
|
|
308
|
-
]);
|
|
493
|
+
return {
|
|
494
|
+
kind: "polyline",
|
|
495
|
+
points,
|
|
496
|
+
startAnchor: null,
|
|
497
|
+
endAnchor: null
|
|
498
|
+
};
|
|
309
499
|
};
|
|
310
500
|
const sectionMidpoint = (section) => {
|
|
311
|
-
|
|
312
|
-
const points = [
|
|
313
|
-
section.startPoint,
|
|
314
|
-
...section.bendPoints ?? [],
|
|
315
|
-
section.endPoint
|
|
316
|
-
];
|
|
501
|
+
const points = sectionPoints(section);
|
|
317
502
|
const mid = points[Math.floor(points.length / 2)];
|
|
318
503
|
return mid ? {
|
|
319
504
|
x: mid.x,
|
|
320
505
|
y: mid.y
|
|
321
506
|
} : null;
|
|
322
507
|
};
|
|
323
|
-
const
|
|
508
|
+
const labelPosition = (edge, section) => {
|
|
509
|
+
const label = edge.labels?.[0];
|
|
324
510
|
if (label?.x != null && label?.y != null) return {
|
|
325
511
|
x: label.x + (label.width ?? 0) / 2,
|
|
326
512
|
y: label.y + (label.height ?? 0) / 2
|
|
327
513
|
};
|
|
328
514
|
return sectionMidpoint(section);
|
|
329
515
|
};
|
|
330
|
-
const
|
|
331
|
-
const getEdgeRouting = (edge, fallback) => {
|
|
516
|
+
const edgeRoutingOf = (edge, fallback) => {
|
|
332
517
|
const options = edge.layoutOptions ?? {};
|
|
333
|
-
const
|
|
334
|
-
return
|
|
518
|
+
const routing = options["elk.edgeRouting"] ?? options["org.eclipse.elk.edgeRouting"];
|
|
519
|
+
return routing === void 0 ? fallback : normalizeEdgeRouting(routing);
|
|
335
520
|
};
|
|
336
|
-
const
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
...layoutPlan.elkOptions
|
|
368
|
-
} : layoutPlan.elkOptions;
|
|
369
|
-
const layoutNodes = [...nodes].sort(compareById);
|
|
370
|
-
const layoutEdges = [...edges].sort(compareEdgesForLayout);
|
|
371
|
-
const graph = await elk.layout({
|
|
372
|
-
id: "root",
|
|
373
|
-
layoutOptions: mergedLayoutOptions,
|
|
374
|
-
children: layoutNodes.map((node) => {
|
|
375
|
-
const nodeData = node.data;
|
|
376
|
-
const rank = nodeData.rank ?? null;
|
|
377
|
-
const layoutOptions = { ...nodeData.layout_attrs ?? {} };
|
|
378
|
-
if (rank === "source") layoutOptions["elk.layered.layering.layerConstraint"] = "FIRST";
|
|
379
|
-
else if (rank === "sink") layoutOptions["elk.layered.layering.layerConstraint"] = "LAST";
|
|
380
|
-
else if (typeof rank === "number") layoutOptions["elk.partitioning.partition"] = String(rank);
|
|
381
|
-
return {
|
|
382
|
-
id: node.id,
|
|
383
|
-
width: nodeData.width,
|
|
384
|
-
height: nodeData.height,
|
|
385
|
-
layoutOptions
|
|
386
|
-
};
|
|
387
|
-
}),
|
|
388
|
-
edges: layoutEdges.map((edge) => {
|
|
389
|
-
const edgeData = edge.data;
|
|
390
|
-
const edgeLabel = edgeData?.label;
|
|
391
|
-
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) => ({
|
|
392
552
|
id: edge.id,
|
|
393
553
|
sources: [edge.source],
|
|
394
554
|
targets: [edge.target],
|
|
395
|
-
layoutOptions:
|
|
396
|
-
...
|
|
397
|
-
text: String(
|
|
398
|
-
width: String(
|
|
555
|
+
layoutOptions: edge.layout_attrs ?? {},
|
|
556
|
+
...edge.label ? { labels: [{
|
|
557
|
+
text: String(edge.label),
|
|
558
|
+
width: String(edge.label).length * 7 + 12,
|
|
399
559
|
height: 18
|
|
400
560
|
}] } : {}
|
|
401
|
-
}
|
|
402
|
-
})
|
|
403
|
-
});
|
|
404
|
-
return {
|
|
405
|
-
positions: toPositionMap(graph.children),
|
|
406
|
-
edgeLayouts: toEdgeLayoutMap(graph.edges, layoutPlan.edgeRouting)
|
|
407
|
-
};
|
|
408
|
-
};
|
|
409
|
-
const useElkLayout = () => {
|
|
410
|
-
const measuredNodes = useNodes();
|
|
411
|
-
const nodesInitialized = useNodesInitialized();
|
|
412
|
-
return useCallback(async (model) => {
|
|
413
|
-
if (!nodesInitialized || !measuredNodesMatchModel(measuredNodes, model)) return null;
|
|
414
|
-
const layout = await runElk({
|
|
415
|
-
nodes: measuredNodes,
|
|
416
|
-
edges: model.edges,
|
|
417
|
-
layoutPlan: model.layoutPlan
|
|
561
|
+
}))
|
|
418
562
|
});
|
|
419
563
|
return {
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
422
581
|
};
|
|
423
|
-
|
|
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
|
+
}
|
|
424
598
|
};
|
|
425
599
|
//#endregion
|
|
426
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/
|
|
600
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/layout/graphviz.ts
|
|
427
601
|
const GRAPHVIZ_DPI = 72;
|
|
428
|
-
const quoteDotValue = (value) => `"${value.replaceAll("\"", "\\\"")}"`;
|
|
429
|
-
const attrsToDot = (attrs) => {
|
|
430
|
-
if (!attrs) return "";
|
|
431
|
-
return Object.entries(attrs).filter(([, value]) => value != null).map(([key, value]) => typeof value === "string" ? `${key}=${quoteDotValue(value)}` : `${key}=${String(value)}`).join(",");
|
|
432
|
-
};
|
|
433
602
|
const parsePoint = (value) => {
|
|
434
603
|
if (!value) return null;
|
|
435
604
|
const [xRaw, yRaw] = value.split(",");
|
|
@@ -441,38 +610,200 @@ const parsePoint = (value) => {
|
|
|
441
610
|
} : null;
|
|
442
611
|
};
|
|
443
612
|
const parseBoundingBoxHeight = (bb) => {
|
|
444
|
-
const height =
|
|
613
|
+
const height = bb?.split(",").map((v) => Number.parseFloat(v))?.[3];
|
|
445
614
|
return Number.isFinite(height) ? height : 0;
|
|
446
615
|
};
|
|
447
|
-
const
|
|
616
|
+
const parseDimension = (value) => {
|
|
448
617
|
if (value == null) return null;
|
|
449
618
|
const dimension = Number.parseFloat(String(value)) * GRAPHVIZ_DPI;
|
|
450
619
|
return Number.isFinite(dimension) ? dimension : null;
|
|
451
620
|
};
|
|
452
|
-
const
|
|
621
|
+
const flipY = (point, graphHeight) => ({
|
|
622
|
+
x: point.x,
|
|
623
|
+
y: graphHeight - point.y
|
|
624
|
+
});
|
|
625
|
+
const parseEdgePosition = (pos) => {
|
|
453
626
|
const result = {
|
|
454
627
|
points: [],
|
|
455
|
-
|
|
456
|
-
|
|
628
|
+
startArrow: null,
|
|
629
|
+
endArrow: null
|
|
457
630
|
};
|
|
458
631
|
if (!pos) return result;
|
|
459
|
-
const
|
|
460
|
-
for (const match of
|
|
632
|
+
const matches = pos.matchAll(/(?:^|\s)(?:(s|e),)?(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g);
|
|
633
|
+
for (const match of matches) {
|
|
461
634
|
const point = {
|
|
462
635
|
x: Number.parseFloat(match[2] ?? "0"),
|
|
463
636
|
y: Number.parseFloat(match[3] ?? "0")
|
|
464
637
|
};
|
|
465
638
|
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) continue;
|
|
466
|
-
if (match[1] === "s") result.
|
|
467
|
-
else if (match[1] === "e") result.
|
|
639
|
+
if (match[1] === "s") result.startArrow = point;
|
|
640
|
+
else if (match[1] === "e") result.endArrow = point;
|
|
468
641
|
else result.points.push(point);
|
|
469
642
|
}
|
|
470
643
|
return result;
|
|
471
644
|
};
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
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)
|
|
475
804
|
});
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/routeToPath.ts
|
|
476
807
|
const buildPolylinePath = (points) => {
|
|
477
808
|
if (points.length < 2) return null;
|
|
478
809
|
return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x},${point.y}`).join(" ");
|
|
@@ -481,643 +812,168 @@ const buildSplinePath = (points) => {
|
|
|
481
812
|
if (points.length < 4 || (points.length - 1) % 3 !== 0) return null;
|
|
482
813
|
let path = `M ${points[0]?.x},${points[0]?.y}`;
|
|
483
814
|
for (let index = 1; index < points.length; index += 3) {
|
|
484
|
-
const
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
if (!
|
|
488
|
-
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}`;
|
|
489
820
|
}
|
|
490
821
|
return path;
|
|
491
822
|
};
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
}
|
|
499
834
|
return buildPolylinePath([
|
|
500
|
-
...
|
|
835
|
+
...startAnchor ? [startAnchor] : [],
|
|
501
836
|
...points,
|
|
502
|
-
...
|
|
837
|
+
...endAnchor ? [endAnchor] : []
|
|
503
838
|
]);
|
|
504
839
|
};
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
fixedsize: node.width && node.height ? true : void 0
|
|
532
|
-
};
|
|
533
|
-
lines.push(`${quoteDotValue(node.id)} [${attrsToDot(attrs)}];`);
|
|
534
|
-
}
|
|
535
|
-
for (const edge of visualization.edges ?? []) {
|
|
536
|
-
const attrs = {
|
|
537
|
-
...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) => ({
|
|
538
866
|
id: edge.id,
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
541
890
|
};
|
|
542
|
-
lines.push(`${quoteDotValue(edge.source)} -> ${quoteDotValue(edge.target)} [${attrsToDot(attrs)}];`);
|
|
543
891
|
}
|
|
544
|
-
|
|
545
|
-
const key = node.rank;
|
|
546
|
-
acc[key] ??= [];
|
|
547
|
-
acc[key].push(quoteDotValue(node.id));
|
|
548
|
-
return acc;
|
|
549
|
-
}, {});
|
|
550
|
-
lines.push(...Object.entries(ranks).map(([rank, ids]) => `{ rank=${["source", "sink"].includes(rank) ? rank : "same"}; ${(ids ?? []).join(" ")} }`));
|
|
551
|
-
lines.push("}");
|
|
552
|
-
return lines.join("\n");
|
|
553
|
-
};
|
|
554
|
-
const getGraphvizEdgeKey = (edge, gvidToName) => {
|
|
555
|
-
const tail = typeof edge.tail === "number" ? gvidToName.get(edge.tail) : edge.tail;
|
|
556
|
-
const head = typeof edge.head === "number" ? gvidToName.get(edge.head) : edge.head;
|
|
557
|
-
return tail && head ? `${tail}->${head}` : null;
|
|
892
|
+
return centers;
|
|
558
893
|
};
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}, {});
|
|
567
|
-
return graphvizEdges.reduce((acc, edge) => {
|
|
568
|
-
const edgeKey = getGraphvizEdgeKey(edge, gvidToName);
|
|
569
|
-
const fallbackIdsForKey = edgeKey ? fallbackIds[edgeKey] : void 0;
|
|
570
|
-
const fallbackIndex = edgeKey ? fallbackCounts.get(edgeKey) ?? 0 : 0;
|
|
571
|
-
const edgeId = edge.id ?? fallbackIdsForKey?.[fallbackIndex];
|
|
572
|
-
if (!edgeId) return acc;
|
|
573
|
-
if (edgeKey) fallbackCounts.set(edgeKey, fallbackIndex + 1);
|
|
574
|
-
const position = parseGraphvizEdgePosition(edge.pos);
|
|
575
|
-
const points = position.points.map((point) => flipY(point, graphHeight));
|
|
576
|
-
const startArrowPoint = position.startArrowPoint ? flipY(position.startArrowPoint, graphHeight) : null;
|
|
577
|
-
const endArrowPoint = position.endArrowPoint ? flipY(position.endArrowPoint, graphHeight) : null;
|
|
578
|
-
const path = buildGraphvizEdgePath({
|
|
579
|
-
points,
|
|
580
|
-
startArrowPoint,
|
|
581
|
-
endArrowPoint
|
|
582
|
-
});
|
|
583
|
-
if (!path) return acc;
|
|
584
|
-
const labelPosition = parsePoint(edge.lp);
|
|
585
|
-
acc[edgeId] = {
|
|
586
|
-
path,
|
|
587
|
-
labelPosition: labelPosition ? flipY(labelPosition, graphHeight) : null,
|
|
588
|
-
startPoint: startArrowPoint ?? points[0] ?? null,
|
|
589
|
-
endPoint: endArrowPoint ?? points.at(-1) ?? null
|
|
590
|
-
};
|
|
591
|
-
return acc;
|
|
592
|
-
}, {});
|
|
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;
|
|
593
901
|
};
|
|
594
|
-
const
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
const graphHeight = parseBoundingBoxHeight(json.bb);
|
|
598
|
-
const gvidToName = /* @__PURE__ */ new Map();
|
|
902
|
+
const routeToEdgeLayout = (route) => {
|
|
903
|
+
const path = routeToPath(route);
|
|
904
|
+
if (!path) return null;
|
|
599
905
|
return {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (!center) return acc;
|
|
605
|
-
const data = nodes.find((candidate) => candidate.id === object.name)?.data;
|
|
606
|
-
const width = data?.width ?? parseGraphvizDimension(object.width) ?? 0;
|
|
607
|
-
const height = data?.height ?? parseGraphvizDimension(object.height) ?? 0;
|
|
608
|
-
const flipped = flipY(center, graphHeight);
|
|
609
|
-
acc[object.name] = {
|
|
610
|
-
x: flipped.x - width / 2,
|
|
611
|
-
y: flipped.y - height / 2
|
|
612
|
-
};
|
|
613
|
-
return acc;
|
|
614
|
-
}, {}),
|
|
615
|
-
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
|
|
616
910
|
};
|
|
617
911
|
};
|
|
618
|
-
const useGraphvizLayout = (visualization) => useCallback(async (model) => {
|
|
619
|
-
const layout = await runGraphviz({
|
|
620
|
-
visualization,
|
|
621
|
-
nodes: model.nodes,
|
|
622
|
-
edges: model.edges,
|
|
623
|
-
layoutPlan: model.layoutPlan
|
|
624
|
-
});
|
|
625
|
-
return {
|
|
626
|
-
nodes: applyNodePositions(model.nodes, layout.positions),
|
|
627
|
-
edges: applyEdgeLayouts(model.edges, layout.edgeLayouts)
|
|
628
|
-
};
|
|
629
|
-
}, [visualization]);
|
|
630
912
|
//#endregion
|
|
631
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/
|
|
632
|
-
const
|
|
913
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/hooks/useGraphLayout.ts
|
|
914
|
+
const prepare = (visualization) => {
|
|
633
915
|
try {
|
|
634
916
|
return {
|
|
635
|
-
model:
|
|
636
|
-
|
|
917
|
+
model: buildGraphModel(visualization),
|
|
918
|
+
engine: selectEngine(visualization),
|
|
919
|
+
error: null
|
|
637
920
|
};
|
|
638
921
|
} catch (err) {
|
|
639
922
|
return {
|
|
640
923
|
model: null,
|
|
641
|
-
|
|
924
|
+
engine: null,
|
|
925
|
+
error: toGraphError(err)
|
|
642
926
|
};
|
|
643
927
|
}
|
|
644
928
|
};
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
const useLayout = (visualization) => {
|
|
648
|
-
const { model, buildError } = useMemo(() => buildSafely(visualization), [visualization]);
|
|
649
|
-
const runElkLayout = useElkLayout();
|
|
650
|
-
const runGraphvizLayout = useGraphvizLayout(visualization);
|
|
929
|
+
const useGraphLayout = (visualization) => {
|
|
930
|
+
const { model, engine, error: prepareError } = useMemo(() => prepare(visualization), [visualization]);
|
|
651
931
|
const [nodes, setNodes] = useState([]);
|
|
652
932
|
const [edges, setEdges] = useState([]);
|
|
653
|
-
const [
|
|
654
|
-
const [
|
|
655
|
-
useEffect(() => {
|
|
656
|
-
setLayoutError(null);
|
|
657
|
-
setNodes(model?.nodes ?? []);
|
|
658
|
-
setEdges(model?.edges ?? []);
|
|
659
|
-
setLayoutReady(!model || model.nodes.length === 0);
|
|
660
|
-
}, [model]);
|
|
933
|
+
const [ready, setReady] = useState(false);
|
|
934
|
+
const [runError, setRunError] = useState(null);
|
|
661
935
|
useEffect(() => {
|
|
662
|
-
|
|
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);
|
|
663
950
|
let cancelled = false;
|
|
664
|
-
(
|
|
665
|
-
if (cancelled
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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);
|
|
669
957
|
}, (err) => {
|
|
670
958
|
if (cancelled) return;
|
|
671
|
-
|
|
672
|
-
|
|
959
|
+
setRunError(toGraphError(err));
|
|
960
|
+
setReady(true);
|
|
673
961
|
});
|
|
674
962
|
return () => {
|
|
675
963
|
cancelled = true;
|
|
676
964
|
};
|
|
677
|
-
}, [
|
|
678
|
-
model,
|
|
679
|
-
buildError,
|
|
680
|
-
layoutReady,
|
|
681
|
-
runElkLayout,
|
|
682
|
-
runGraphvizLayout
|
|
683
|
-
]);
|
|
965
|
+
}, [model, engine]);
|
|
684
966
|
const onNodesChange = useCallback((changes) => setNodes((current) => applyNodeChanges(changes, current)), []);
|
|
685
967
|
return {
|
|
686
968
|
nodes,
|
|
687
969
|
edges,
|
|
688
|
-
|
|
689
|
-
error:
|
|
970
|
+
ready,
|
|
971
|
+
error: prepareError ?? runError,
|
|
690
972
|
onNodesChange
|
|
691
973
|
};
|
|
692
974
|
};
|
|
693
975
|
//#endregion
|
|
694
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/render/
|
|
695
|
-
const HIDDEN_HANDLE_STYLE = {
|
|
696
|
-
opacity: 0,
|
|
697
|
-
pointerEvents: "none"
|
|
698
|
-
};
|
|
699
|
-
const HiddenHandles = () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Handle, {
|
|
700
|
-
type: "target",
|
|
701
|
-
position: Position.Left,
|
|
702
|
-
style: HIDDEN_HANDLE_STYLE
|
|
703
|
-
}), /* @__PURE__ */ jsx(Handle, {
|
|
704
|
-
type: "source",
|
|
705
|
-
position: Position.Right,
|
|
706
|
-
style: HIDDEN_HANDLE_STYLE
|
|
707
|
-
})] });
|
|
708
|
-
const AnnotationBadge = () => /* @__PURE__ */ jsx(Badge, {
|
|
709
|
-
size: "xs",
|
|
710
|
-
variant: "filled",
|
|
711
|
-
color: "gray",
|
|
712
|
-
title: "This graph element has an annotation visualization.",
|
|
713
|
-
children: "i"
|
|
714
|
-
});
|
|
715
|
-
const NodeAnnotation = ({ annotation }) => {
|
|
716
|
-
const [opened, setOpened] = useState(false);
|
|
717
|
-
const toggle = useCallback((e) => {
|
|
718
|
-
e.stopPropagation();
|
|
719
|
-
setOpened((o) => !o);
|
|
720
|
-
}, []);
|
|
721
|
-
return /* @__PURE__ */ jsxs(Popover, {
|
|
722
|
-
opened,
|
|
723
|
-
onClose: () => setOpened(false),
|
|
724
|
-
position: "right",
|
|
725
|
-
withArrow: true,
|
|
726
|
-
withinPortal: true,
|
|
727
|
-
shadow: "md",
|
|
728
|
-
children: [/* @__PURE__ */ jsx(Popover.Target, { children: /* @__PURE__ */ jsx("button", {
|
|
729
|
-
type: "button",
|
|
730
|
-
className: "nodrag nopan",
|
|
731
|
-
style: {
|
|
732
|
-
position: "absolute",
|
|
733
|
-
right: -8,
|
|
734
|
-
top: -8,
|
|
735
|
-
cursor: "pointer",
|
|
736
|
-
pointerEvents: "all",
|
|
737
|
-
zIndex: 10,
|
|
738
|
-
background: "none",
|
|
739
|
-
border: "none",
|
|
740
|
-
padding: 0
|
|
741
|
-
},
|
|
742
|
-
onClick: toggle,
|
|
743
|
-
onMouseDown: (e) => e.stopPropagation(),
|
|
744
|
-
children: /* @__PURE__ */ jsx(AnnotationBadge, {})
|
|
745
|
-
}) }), /* @__PURE__ */ jsxs(Popover.Dropdown, {
|
|
746
|
-
style: {
|
|
747
|
-
minWidth: 200,
|
|
748
|
-
maxWidth: 400
|
|
749
|
-
},
|
|
750
|
-
children: [/* @__PURE__ */ jsx(Group, {
|
|
751
|
-
justify: "flex-end",
|
|
752
|
-
mb: 4,
|
|
753
|
-
children: /* @__PURE__ */ jsx(ActionIcon, {
|
|
754
|
-
size: "xs",
|
|
755
|
-
variant: "subtle",
|
|
756
|
-
color: "gray",
|
|
757
|
-
onClick: () => setOpened(false),
|
|
758
|
-
children: /* @__PURE__ */ jsx(XIcon, { size: 12 })
|
|
759
|
-
})
|
|
760
|
-
}), /* @__PURE__ */ jsx(ScrollArea.Autosize, {
|
|
761
|
-
mah: 300,
|
|
762
|
-
children: /* @__PURE__ */ jsx(Visualization, { visualization: annotation })
|
|
763
|
-
})]
|
|
764
|
-
})]
|
|
765
|
-
});
|
|
766
|
-
};
|
|
767
|
-
const ExternalLabel = ({ children, placement }) => /* @__PURE__ */ jsx(Text, {
|
|
768
|
-
size: "xs",
|
|
769
|
-
fw: 600,
|
|
770
|
-
style: {
|
|
771
|
-
position: "absolute",
|
|
772
|
-
[placement === "top" ? "bottom" : "top"]: "100%",
|
|
773
|
-
left: "50%",
|
|
774
|
-
transform: `translate(-50%, ${placement === "top" ? "-4px" : "4px"})`,
|
|
775
|
-
whiteSpace: "nowrap",
|
|
776
|
-
color: DEFAULT_COLORS.text,
|
|
777
|
-
pointerEvents: "none",
|
|
778
|
-
letterSpacing: "0.01em"
|
|
779
|
-
},
|
|
780
|
-
children
|
|
781
|
-
});
|
|
782
|
-
const InnerSymbol = ({ symbol, color }) => {
|
|
783
|
-
if (symbol === "triangle") return /* @__PURE__ */ jsx("div", { style: {
|
|
784
|
-
width: 0,
|
|
785
|
-
height: 0,
|
|
786
|
-
borderTop: `${12 * .6}px solid transparent`,
|
|
787
|
-
borderBottom: `${12 * .6}px solid transparent`,
|
|
788
|
-
borderLeft: `12px solid ${color}`,
|
|
789
|
-
marginLeft: 2
|
|
790
|
-
} });
|
|
791
|
-
return /* @__PURE__ */ jsx("div", { style: {
|
|
792
|
-
width: 12,
|
|
793
|
-
height: 12,
|
|
794
|
-
backgroundColor: color
|
|
795
|
-
} });
|
|
796
|
-
};
|
|
797
|
-
const CLIP_PATH = {
|
|
798
|
-
triangle: "polygon(50% 0, 100% 100%, 0 100%)",
|
|
799
|
-
diamond: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)",
|
|
800
|
-
hexagon: "polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%)"
|
|
801
|
-
};
|
|
802
|
-
const CircleShape = ({ width, height, color, borderColor, doubleBorder, innerSymbol, initialTokens, finalTokens, centerLabel }) => /* @__PURE__ */ jsxs("div", {
|
|
803
|
-
style: {
|
|
804
|
-
position: "relative",
|
|
805
|
-
boxSizing: "border-box",
|
|
806
|
-
width,
|
|
807
|
-
height,
|
|
808
|
-
borderRadius: "50%",
|
|
809
|
-
backgroundColor: color,
|
|
810
|
-
border: borderColor ? `2px solid ${borderColor}` : "none",
|
|
811
|
-
display: "flex",
|
|
812
|
-
flexDirection: "column",
|
|
813
|
-
alignItems: "center",
|
|
814
|
-
justifyContent: "center",
|
|
815
|
-
gap: 2
|
|
816
|
-
},
|
|
817
|
-
children: [
|
|
818
|
-
doubleBorder && /* @__PURE__ */ jsx("div", { style: {
|
|
819
|
-
position: "absolute",
|
|
820
|
-
inset: 5,
|
|
821
|
-
borderRadius: "50%",
|
|
822
|
-
border: borderColor ? `2px solid ${borderColor}` : "none",
|
|
823
|
-
pointerEvents: "none"
|
|
824
|
-
} }),
|
|
825
|
-
initialTokens && !innerSymbol && (initialTokens === 1 ? /* @__PURE__ */ jsx("div", { style: {
|
|
826
|
-
width: 12,
|
|
827
|
-
height: 12,
|
|
828
|
-
borderRadius: "50%",
|
|
829
|
-
backgroundColor: "rgba(0,0,0,0.65)"
|
|
830
|
-
} }) : /* @__PURE__ */ jsx("span", {
|
|
831
|
-
style: {
|
|
832
|
-
color: "#111",
|
|
833
|
-
fontSize: 11,
|
|
834
|
-
fontWeight: 700,
|
|
835
|
-
lineHeight: 1
|
|
836
|
-
},
|
|
837
|
-
children: initialTokens
|
|
838
|
-
})),
|
|
839
|
-
finalTokens && finalTokens > 1 && /* @__PURE__ */ jsx("span", {
|
|
840
|
-
style: {
|
|
841
|
-
position: "absolute",
|
|
842
|
-
right: 4,
|
|
843
|
-
bottom: 3,
|
|
844
|
-
color: "#111",
|
|
845
|
-
fontSize: 9,
|
|
846
|
-
fontWeight: 700,
|
|
847
|
-
lineHeight: 1
|
|
848
|
-
},
|
|
849
|
-
children: finalTokens
|
|
850
|
-
}),
|
|
851
|
-
innerSymbol && /* @__PURE__ */ jsx(InnerSymbol, {
|
|
852
|
-
symbol: innerSymbol,
|
|
853
|
-
color: borderColor ?? "#111"
|
|
854
|
-
}),
|
|
855
|
-
centerLabel && /* @__PURE__ */ jsx("span", {
|
|
856
|
-
style: {
|
|
857
|
-
maxWidth: width - 6,
|
|
858
|
-
color: "#111",
|
|
859
|
-
fontSize: 10,
|
|
860
|
-
fontWeight: 600,
|
|
861
|
-
lineHeight: 1,
|
|
862
|
-
overflow: "hidden",
|
|
863
|
-
textAlign: "center",
|
|
864
|
-
textOverflow: "ellipsis",
|
|
865
|
-
whiteSpace: "nowrap"
|
|
866
|
-
},
|
|
867
|
-
children: centerLabel
|
|
868
|
-
})
|
|
869
|
-
]
|
|
870
|
-
});
|
|
871
|
-
const BoxShape = ({ shape, color, borderColor, doubleBorder, innerSymbol, width, height, children }) => {
|
|
872
|
-
const clipPath = CLIP_PATH[shape];
|
|
873
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
874
|
-
style: {
|
|
875
|
-
position: "relative",
|
|
876
|
-
boxSizing: "border-box",
|
|
877
|
-
width: width ?? void 0,
|
|
878
|
-
height: height ?? void 0,
|
|
879
|
-
padding: borderColor != null ? 2 : 0,
|
|
880
|
-
backgroundColor: borderColor ?? "transparent",
|
|
881
|
-
borderRadius: shape === "rectangle" ? 5 : 0,
|
|
882
|
-
clipPath
|
|
883
|
-
},
|
|
884
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
885
|
-
style: {
|
|
886
|
-
boxSizing: "border-box",
|
|
887
|
-
width: "100%",
|
|
888
|
-
height: "100%",
|
|
889
|
-
backgroundColor: color,
|
|
890
|
-
borderRadius: shape === "rectangle" ? 3 : 0,
|
|
891
|
-
clipPath,
|
|
892
|
-
display: "flex",
|
|
893
|
-
alignItems: "center",
|
|
894
|
-
justifyContent: "center",
|
|
895
|
-
overflow: "hidden"
|
|
896
|
-
},
|
|
897
|
-
children: innerSymbol && !children ? /* @__PURE__ */ jsx(InnerSymbol, {
|
|
898
|
-
symbol: innerSymbol,
|
|
899
|
-
color: "#111"
|
|
900
|
-
}) : children
|
|
901
|
-
}), doubleBorder && /* @__PURE__ */ jsx("div", { style: {
|
|
902
|
-
position: "absolute",
|
|
903
|
-
inset: 6,
|
|
904
|
-
border: borderColor ? `1.5px solid ${borderColor}` : "none",
|
|
905
|
-
borderRadius: shape === "rectangle" ? 3 : 0,
|
|
906
|
-
clipPath,
|
|
907
|
-
pointerEvents: "none"
|
|
908
|
-
} })]
|
|
909
|
-
});
|
|
910
|
-
};
|
|
911
|
-
const GraphFlowNode = memo(({ data }) => {
|
|
912
|
-
const { shape, label, color, border_color, annotation, label_pos, width, height, style } = data;
|
|
913
|
-
const doubleBorder = style?.double_border ?? null;
|
|
914
|
-
const innerSymbol = style?.inner_symbol ?? null;
|
|
915
|
-
const initialTokens = style?.initial_tokens ?? null;
|
|
916
|
-
const finalTokens = style?.final_tokens ?? null;
|
|
917
|
-
const isExternalLabel = Boolean(label && label_pos !== "center" && label_pos != null);
|
|
918
|
-
const maxLabelLines = height != null ? Math.max(1, Math.floor((height - 4) / (14 * 1.2))) : void 0;
|
|
919
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
920
|
-
style: {
|
|
921
|
-
position: "relative",
|
|
922
|
-
width: shape === "circle" ? width ?? void 0 : void 0
|
|
923
|
-
},
|
|
924
|
-
children: [
|
|
925
|
-
/* @__PURE__ */ jsx(HiddenHandles, {}),
|
|
926
|
-
isExternalLabel && label_pos === "top" && /* @__PURE__ */ jsx(ExternalLabel, {
|
|
927
|
-
placement: "top",
|
|
928
|
-
children: label
|
|
929
|
-
}),
|
|
930
|
-
shape === "circle" ? /* @__PURE__ */ jsx(CircleShape, {
|
|
931
|
-
width,
|
|
932
|
-
height,
|
|
933
|
-
color: color ?? DEFAULT_COLORS.transition,
|
|
934
|
-
borderColor: border_color ?? null,
|
|
935
|
-
doubleBorder,
|
|
936
|
-
innerSymbol,
|
|
937
|
-
initialTokens,
|
|
938
|
-
finalTokens,
|
|
939
|
-
centerLabel: label_pos === "center" ? label ?? null : null
|
|
940
|
-
}) : /* @__PURE__ */ jsx(BoxShape, {
|
|
941
|
-
shape,
|
|
942
|
-
color: color ?? DEFAULT_COLORS.transition,
|
|
943
|
-
borderColor: border_color ?? null,
|
|
944
|
-
doubleBorder,
|
|
945
|
-
innerSymbol,
|
|
946
|
-
width: width ?? null,
|
|
947
|
-
height: height ?? null,
|
|
948
|
-
children: !isExternalLabel && label && /* @__PURE__ */ jsx(Text, {
|
|
949
|
-
size: "sm",
|
|
950
|
-
fw: 500,
|
|
951
|
-
ta: "center",
|
|
952
|
-
px: 12,
|
|
953
|
-
style: {
|
|
954
|
-
lineHeight: 1.2,
|
|
955
|
-
color: "#111",
|
|
956
|
-
maxWidth: width != null ? width : "100%",
|
|
957
|
-
boxSizing: "border-box",
|
|
958
|
-
overflow: "hidden",
|
|
959
|
-
overflowWrap: "anywhere",
|
|
960
|
-
...maxLabelLines != null ? {
|
|
961
|
-
display: "-webkit-box",
|
|
962
|
-
WebkitBoxOrient: "vertical",
|
|
963
|
-
WebkitLineClamp: maxLabelLines
|
|
964
|
-
} : { whiteSpace: "normal" }
|
|
965
|
-
},
|
|
966
|
-
children: label
|
|
967
|
-
})
|
|
968
|
-
}),
|
|
969
|
-
isExternalLabel && label_pos === "bottom" && /* @__PURE__ */ jsx(ExternalLabel, {
|
|
970
|
-
placement: "bottom",
|
|
971
|
-
children: label
|
|
972
|
-
}),
|
|
973
|
-
annotation && /* @__PURE__ */ jsx(NodeAnnotation, { annotation })
|
|
974
|
-
]
|
|
975
|
-
});
|
|
976
|
-
});
|
|
977
|
-
GraphFlowNode.displayName = "GraphFlowNode";
|
|
978
|
-
//#endregion
|
|
979
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/render/Edge.tsx
|
|
980
|
-
const placeBounds = (node) => {
|
|
981
|
-
const data = node.data;
|
|
982
|
-
const width = data.width ?? 0;
|
|
983
|
-
const height = data.height ?? 0;
|
|
984
|
-
return {
|
|
985
|
-
x: node.internals.positionAbsolute.x,
|
|
986
|
-
y: node.internals.positionAbsolute.y,
|
|
987
|
-
width,
|
|
988
|
-
height
|
|
989
|
-
};
|
|
990
|
-
};
|
|
991
|
-
const visualBounds = (node) => {
|
|
992
|
-
if (node.data.shape === "circle") return placeBounds(node);
|
|
993
|
-
return {
|
|
994
|
-
x: node.internals.positionAbsolute.x,
|
|
995
|
-
y: node.internals.positionAbsolute.y,
|
|
996
|
-
width: node.measured.width ?? 0,
|
|
997
|
-
height: node.measured.height ?? 0
|
|
998
|
-
};
|
|
999
|
-
};
|
|
1000
|
-
const center = (node) => {
|
|
1001
|
-
const bounds = visualBounds(node);
|
|
1002
|
-
return {
|
|
1003
|
-
x: bounds.x + bounds.width / 2,
|
|
1004
|
-
y: bounds.y + bounds.height / 2
|
|
1005
|
-
};
|
|
1006
|
-
};
|
|
1007
|
-
const rectBorderPoint = (node, other) => {
|
|
1008
|
-
const bounds = visualBounds(node);
|
|
1009
|
-
const hw = bounds.width / 2;
|
|
1010
|
-
const hh = bounds.height / 2;
|
|
1011
|
-
if (hw === 0 || hh === 0) return null;
|
|
1012
|
-
const nc = center(node);
|
|
1013
|
-
const oc = center(other);
|
|
1014
|
-
const dx = oc.x - nc.x;
|
|
1015
|
-
const dy = oc.y - nc.y;
|
|
1016
|
-
const xx = dx / (2 * hw) - dy / (2 * hh);
|
|
1017
|
-
const yy = dx / (2 * hw) + dy / (2 * hh);
|
|
1018
|
-
const scale = 1 / (Math.abs(xx) + Math.abs(yy));
|
|
1019
|
-
if (!Number.isFinite(scale)) return null;
|
|
1020
|
-
return {
|
|
1021
|
-
x: nc.x + hw * scale * (xx + yy),
|
|
1022
|
-
y: nc.y + hh * scale * (-xx + yy)
|
|
1023
|
-
};
|
|
1024
|
-
};
|
|
1025
|
-
const circleBorderPoint = (node, other) => {
|
|
1026
|
-
const bounds = placeBounds(node);
|
|
1027
|
-
const rx = bounds.width / 2;
|
|
1028
|
-
const ry = bounds.height / 2;
|
|
1029
|
-
if (rx === 0 || ry === 0) return null;
|
|
1030
|
-
const nc = center(node);
|
|
1031
|
-
const oc = center(other);
|
|
1032
|
-
const angle = Math.atan2(oc.y - nc.y, oc.x - nc.x);
|
|
1033
|
-
return {
|
|
1034
|
-
x: nc.x + Math.cos(angle) * rx,
|
|
1035
|
-
y: nc.y + Math.sin(angle) * ry
|
|
1036
|
-
};
|
|
1037
|
-
};
|
|
1038
|
-
const borderPoint = (node, other) => node.data.shape === "circle" ? circleBorderPoint(node, other) : rectBorderPoint(node, other);
|
|
1039
|
-
const edgeSide = (node, pt) => {
|
|
1040
|
-
const nc = center(node);
|
|
1041
|
-
const dx = pt.x - nc.x;
|
|
1042
|
-
const dy = pt.y - nc.y;
|
|
1043
|
-
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? Position.Right : Position.Left;
|
|
1044
|
-
return dy >= 0 ? Position.Bottom : Position.Top;
|
|
1045
|
-
};
|
|
1046
|
-
const getEdgeParams = (source, target) => {
|
|
1047
|
-
const sp = borderPoint(source, target);
|
|
1048
|
-
const tp = borderPoint(target, source);
|
|
1049
|
-
if (!sp || !tp) return null;
|
|
1050
|
-
return {
|
|
1051
|
-
sx: sp.x,
|
|
1052
|
-
sy: sp.y,
|
|
1053
|
-
tx: tp.x,
|
|
1054
|
-
ty: tp.y,
|
|
1055
|
-
sourcePos: edgeSide(source, sp),
|
|
1056
|
-
targetPos: edgeSide(target, tp)
|
|
1057
|
-
};
|
|
1058
|
-
};
|
|
1059
|
-
const fallbackLabelPosition = ({ sourceX, sourceY, targetX, targetY }) => ({
|
|
1060
|
-
x: (sourceX + targetX) / 2,
|
|
1061
|
-
y: (sourceY + targetY) / 2
|
|
1062
|
-
});
|
|
1063
|
-
const getSelfLoopPath = (sourceNode) => {
|
|
1064
|
-
const width = sourceNode.measured.width ?? 60;
|
|
1065
|
-
const height = sourceNode.measured.height ?? 34;
|
|
1066
|
-
const position = sourceNode.internals.positionAbsolute;
|
|
1067
|
-
const [path, labelX, labelY] = getSmoothStepPath({
|
|
1068
|
-
sourceX: position.x + width,
|
|
1069
|
-
sourceY: position.y + height * .25,
|
|
1070
|
-
sourcePosition: "right",
|
|
1071
|
-
targetX: position.x + width,
|
|
1072
|
-
targetY: position.y + height * .75,
|
|
1073
|
-
targetPosition: "right",
|
|
1074
|
-
borderRadius: 20,
|
|
1075
|
-
offset: 50
|
|
1076
|
-
});
|
|
1077
|
-
return {
|
|
1078
|
-
path,
|
|
1079
|
-
labelX,
|
|
1080
|
-
labelY
|
|
1081
|
-
};
|
|
1082
|
-
};
|
|
1083
|
-
const getFloatingPath = ({ sourceNode, targetNode, fallback }) => {
|
|
1084
|
-
const params = sourceNode && targetNode ? getEdgeParams(sourceNode, targetNode) : null;
|
|
1085
|
-
const [path, labelX, labelY] = getBezierPath(params ? {
|
|
1086
|
-
sourceX: params.sx,
|
|
1087
|
-
sourceY: params.sy,
|
|
1088
|
-
sourcePosition: params.sourcePos,
|
|
1089
|
-
targetX: params.tx,
|
|
1090
|
-
targetY: params.ty,
|
|
1091
|
-
targetPosition: params.targetPos
|
|
1092
|
-
} : fallback);
|
|
1093
|
-
return {
|
|
1094
|
-
path,
|
|
1095
|
-
labelX,
|
|
1096
|
-
labelY
|
|
1097
|
-
};
|
|
1098
|
-
};
|
|
1099
|
-
const resolveEdgePath = ({ edge, sourceNode, targetNode }) => {
|
|
1100
|
-
const { data, source, target, sourceX, sourceY, targetX, targetY } = edge;
|
|
1101
|
-
const customLabelPosition = fallbackLabelPosition(edge);
|
|
1102
|
-
if (data?.path) return {
|
|
1103
|
-
path: data.path,
|
|
1104
|
-
labelX: data.labelPosition?.x ?? customLabelPosition.x,
|
|
1105
|
-
labelY: data.labelPosition?.y ?? customLabelPosition.y
|
|
1106
|
-
};
|
|
1107
|
-
if (source === target && sourceNode) return getSelfLoopPath(sourceNode);
|
|
1108
|
-
return getFloatingPath({
|
|
1109
|
-
sourceNode,
|
|
1110
|
-
targetNode,
|
|
1111
|
-
fallback: {
|
|
1112
|
-
sourceX,
|
|
1113
|
-
sourceY,
|
|
1114
|
-
sourcePosition: edge.sourcePosition,
|
|
1115
|
-
targetX,
|
|
1116
|
-
targetY,
|
|
1117
|
-
targetPosition: edge.targetPosition
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
};
|
|
976
|
+
//#region src/components/ResourceView/Viewers/GraphFlow/render/edgeArrows.tsx
|
|
1121
977
|
const colorToId = (color) => color.replace(/[^a-zA-Z0-9]/g, "");
|
|
1122
978
|
const ArrowMarker = ({ id, type, color, isStart }) => {
|
|
1123
979
|
if (!type) return null;
|
|
@@ -1284,6 +1140,69 @@ const getMarkerIds = ({ color, startArrow, endArrow }) => {
|
|
|
1284
1140
|
endMarkerId: endArrow ? `arrow-end-${endArrow}-${colorKey}` : null
|
|
1285
1141
|
};
|
|
1286
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
|
|
1287
1206
|
const EdgeLabel = ({ color, label, hasAnnotation, x, y }) => /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs("div", {
|
|
1288
1207
|
style: {
|
|
1289
1208
|
position: "absolute",
|
|
@@ -1318,8 +1237,6 @@ const EdgeEndLabel = ({ label, x, y }) => /* @__PURE__ */ jsx(EdgeLabelRenderer,
|
|
|
1318
1237
|
className: "nodrag nopan",
|
|
1319
1238
|
children: label
|
|
1320
1239
|
}) });
|
|
1321
|
-
const END_LABEL_INSET = 22;
|
|
1322
|
-
const END_LABEL_PERP = 9;
|
|
1323
1240
|
const endLabelPositions = (start, end) => {
|
|
1324
1241
|
const dx = end.x - start.x;
|
|
1325
1242
|
const dy = end.y - start.y;
|
|
@@ -1330,40 +1247,82 @@ const endLabelPositions = (start, end) => {
|
|
|
1330
1247
|
const py = ux;
|
|
1331
1248
|
return {
|
|
1332
1249
|
start: {
|
|
1333
|
-
x: start.x + ux *
|
|
1334
|
-
y: start.y + uy *
|
|
1250
|
+
x: start.x + ux * 22 + px * 9,
|
|
1251
|
+
y: start.y + uy * 22 + py * 9
|
|
1335
1252
|
},
|
|
1336
1253
|
end: {
|
|
1337
|
-
x: end.x - ux *
|
|
1338
|
-
y: end.y - uy *
|
|
1254
|
+
x: end.x - ux * 22 + px * 9,
|
|
1255
|
+
y: end.y - uy * 22 + py * 9
|
|
1339
1256
|
}
|
|
1340
1257
|
};
|
|
1341
1258
|
};
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
const
|
|
1351
|
-
const
|
|
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);
|
|
1308
|
+
if (!data) return null;
|
|
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;
|
|
1352
1315
|
const { startMarkerId, endMarkerId } = getMarkerIds({
|
|
1353
1316
|
color,
|
|
1354
1317
|
startArrow,
|
|
1355
1318
|
endArrow
|
|
1356
1319
|
});
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
sourceNode,
|
|
1360
|
-
targetNode
|
|
1361
|
-
});
|
|
1362
|
-
const startPos = data.startPoint ?? {
|
|
1320
|
+
const resolved = resolvePath(props, layout, sourceNode);
|
|
1321
|
+
const startPos = layout?.startPoint ?? {
|
|
1363
1322
|
x: sourceX,
|
|
1364
1323
|
y: sourceY
|
|
1365
1324
|
};
|
|
1366
|
-
const endPos =
|
|
1325
|
+
const endPos = layout?.endPoint ?? {
|
|
1367
1326
|
x: targetX,
|
|
1368
1327
|
y: targetY
|
|
1369
1328
|
};
|
|
@@ -1381,7 +1340,7 @@ const GraphFlowEdge = memo((props) => {
|
|
|
1381
1340
|
})] }),
|
|
1382
1341
|
/* @__PURE__ */ jsx(BaseEdge, {
|
|
1383
1342
|
id,
|
|
1384
|
-
path:
|
|
1343
|
+
path: resolved.path,
|
|
1385
1344
|
...endMarkerId ? { markerEnd: `url(#${endMarkerId})` } : {},
|
|
1386
1345
|
...startMarkerId ? { markerStart: `url(#${startMarkerId})` } : {},
|
|
1387
1346
|
style: {
|
|
@@ -1390,21 +1349,21 @@ const GraphFlowEdge = memo((props) => {
|
|
|
1390
1349
|
strokeDasharray: dashed ? "6 3" : void 0
|
|
1391
1350
|
}
|
|
1392
1351
|
}),
|
|
1393
|
-
(
|
|
1352
|
+
(model.label || model.annotation) && /* @__PURE__ */ jsx(EdgeLabel, {
|
|
1394
1353
|
color,
|
|
1395
|
-
label:
|
|
1396
|
-
hasAnnotation: Boolean(
|
|
1397
|
-
x:
|
|
1398
|
-
y:
|
|
1354
|
+
label: model.label,
|
|
1355
|
+
hasAnnotation: Boolean(model.annotation),
|
|
1356
|
+
x: resolved.labelX,
|
|
1357
|
+
y: resolved.labelY
|
|
1399
1358
|
}),
|
|
1400
|
-
(
|
|
1359
|
+
(model.start_label || model.end_label) && (() => {
|
|
1401
1360
|
const positions = endLabelPositions(startPos, endPos);
|
|
1402
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1403
|
-
label:
|
|
1361
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [model.start_label && /* @__PURE__ */ jsx(EdgeEndLabel, {
|
|
1362
|
+
label: model.start_label,
|
|
1404
1363
|
x: positions.start.x,
|
|
1405
1364
|
y: positions.start.y
|
|
1406
|
-
}),
|
|
1407
|
-
label:
|
|
1365
|
+
}), model.end_label && /* @__PURE__ */ jsx(EdgeEndLabel, {
|
|
1366
|
+
label: model.end_label,
|
|
1408
1367
|
x: positions.end.x,
|
|
1409
1368
|
y: positions.end.y
|
|
1410
1369
|
})] });
|
|
@@ -1413,202 +1372,242 @@ const GraphFlowEdge = memo((props) => {
|
|
|
1413
1372
|
});
|
|
1414
1373
|
GraphFlowEdge.displayName = "GraphFlowEdge";
|
|
1415
1374
|
//#endregion
|
|
1416
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/
|
|
1417
|
-
|
|
1418
|
-
if (
|
|
1419
|
-
x: 0,
|
|
1420
|
-
y: 0,
|
|
1421
|
-
width: 0,
|
|
1422
|
-
height: 0
|
|
1423
|
-
};
|
|
1424
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1425
|
-
for (const node of nodes) {
|
|
1426
|
-
const x = node.position.x;
|
|
1427
|
-
const y = node.position.y;
|
|
1428
|
-
const w = node.measured?.width ?? 0;
|
|
1429
|
-
const h = node.measured?.height ?? 0;
|
|
1430
|
-
minX = Math.min(minX, x);
|
|
1431
|
-
minY = Math.min(minY, y);
|
|
1432
|
-
maxX = Math.max(maxX, x + w);
|
|
1433
|
-
maxY = Math.max(maxY, y + h);
|
|
1434
|
-
}
|
|
1435
|
-
return Number.isFinite(minX) ? {
|
|
1436
|
-
x: minX,
|
|
1437
|
-
y: minY,
|
|
1438
|
-
width: maxX - minX,
|
|
1439
|
-
height: maxY - minY
|
|
1440
|
-
} : {
|
|
1441
|
-
x: 0,
|
|
1442
|
-
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: {
|
|
1443
1378
|
width: 0,
|
|
1444
|
-
height: 0
|
|
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
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
+
};
|
|
1541
1505
|
//#endregion
|
|
1542
|
-
//#region src/components/ResourceView/Viewers/GraphFlow/
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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", {
|
|
1552
1544
|
style: {
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
transform: `translate(${x}px, ${y}px) scale(${zoom})`
|
|
1545
|
+
position: "relative",
|
|
1546
|
+
width: shape === "circle" ? width ?? void 0 : void 0
|
|
1556
1547
|
},
|
|
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
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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
|
+
]
|
|
1600
1599
|
});
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1600
|
+
});
|
|
1601
|
+
GraphFlowNode.displayName = "GraphFlowNode";
|
|
1604
1602
|
//#endregion
|
|
1605
1603
|
//#region src/components/ResourceView/Viewers/GraphFlow/index.tsx
|
|
1606
1604
|
const nodeTypes = { node: GraphFlowNode };
|
|
1607
1605
|
const edgeTypes = { graphflow: GraphFlowEdge };
|
|
1608
1606
|
const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
1609
|
-
const { nodes, edges,
|
|
1610
|
-
const { fitBounds, getNodes, getEdges
|
|
1607
|
+
const { nodes, edges, ready, error, onNodesChange } = useGraphLayout(visualization);
|
|
1608
|
+
const { fitBounds, getNodes, getEdges } = useReactFlow();
|
|
1611
1609
|
const wrapperRef = useRef(null);
|
|
1610
|
+
const { downloadPng, downloadSvg, downloadPdf } = useGraphExport(wrapperRef);
|
|
1612
1611
|
const fitViewWithEdges = useCallback(() => {
|
|
1613
1612
|
fitBounds(computeAutoFitBounds(getNodes(), getEdges()), { padding: FIT_VIEW_PADDING });
|
|
1614
1613
|
}, [
|
|
@@ -1616,19 +1615,16 @@ const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
|
1616
1615
|
getNodes,
|
|
1617
1616
|
getEdges
|
|
1618
1617
|
]);
|
|
1619
|
-
const handleDownloadPng = useCallback(() => wrapperRef.current && downloadPng(wrapperRef.current, getNodes(), getNodesBounds), [getNodes, getNodesBounds]);
|
|
1620
|
-
const handleDownloadSvg = useCallback(() => wrapperRef.current && downloadSvg(wrapperRef.current, getNodes(), getNodesBounds), [getNodes, getNodesBounds]);
|
|
1621
|
-
const handleDownloadPdf = useCallback(() => wrapperRef.current && downloadPdf(wrapperRef.current, getNodes(), getNodesBounds), [getNodes, getNodesBounds]);
|
|
1622
1618
|
const wasReadyRef = useRef(false);
|
|
1623
1619
|
useEffect(() => {
|
|
1624
|
-
if (!
|
|
1620
|
+
if (!ready) {
|
|
1625
1621
|
wasReadyRef.current = false;
|
|
1626
1622
|
return;
|
|
1627
1623
|
}
|
|
1628
1624
|
if (wasReadyRef.current) return;
|
|
1629
1625
|
wasReadyRef.current = true;
|
|
1630
1626
|
fitViewWithEdges();
|
|
1631
|
-
}, [
|
|
1627
|
+
}, [ready, fitViewWithEdges]);
|
|
1632
1628
|
if (error) return /* @__PURE__ */ jsx(Stack, {
|
|
1633
1629
|
h: "100%",
|
|
1634
1630
|
justify: "center",
|
|
@@ -1644,16 +1640,16 @@ const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
|
1644
1640
|
})
|
|
1645
1641
|
});
|
|
1646
1642
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LoadingOverlay, {
|
|
1647
|
-
visible: !
|
|
1643
|
+
visible: !ready,
|
|
1648
1644
|
zIndex: 10
|
|
1649
1645
|
}), /* @__PURE__ */ jsx("div", {
|
|
1650
1646
|
ref: wrapperRef,
|
|
1651
1647
|
style: {
|
|
1652
1648
|
width: "100%",
|
|
1653
1649
|
height: "100%",
|
|
1654
|
-
opacity:
|
|
1650
|
+
opacity: ready ? 1 : 0
|
|
1655
1651
|
},
|
|
1656
|
-
children: /* @__PURE__ */
|
|
1652
|
+
children: /* @__PURE__ */ jsxs(ReactFlow, {
|
|
1657
1653
|
nodes,
|
|
1658
1654
|
edges,
|
|
1659
1655
|
nodeTypes,
|
|
@@ -1666,50 +1662,18 @@ const GraphFlowCanvas = ({ visualization, isPreview, menuItems }) => {
|
|
|
1666
1662
|
panOnDrag: !isPreview,
|
|
1667
1663
|
minZoom: .05,
|
|
1668
1664
|
maxZoom: 3,
|
|
1669
|
-
children: !isPreview && /* @__PURE__ */
|
|
1665
|
+
children: [!isPreview && /* @__PURE__ */ jsx(Background, {
|
|
1670
1666
|
color: "#e5e7eb",
|
|
1671
1667
|
gap: 20,
|
|
1672
1668
|
size: 1
|
|
1673
|
-
}), /* @__PURE__ */
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
}),
|
|
1682
|
-
/* @__PURE__ */ jsxs(Menu, {
|
|
1683
|
-
position: "right",
|
|
1684
|
-
withinPortal: false,
|
|
1685
|
-
children: [/* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(ActionIcon, {
|
|
1686
|
-
variant: "transparent",
|
|
1687
|
-
children: /* @__PURE__ */ jsx(DownloadIcon, {
|
|
1688
|
-
color: "black",
|
|
1689
|
-
size: 18
|
|
1690
|
-
})
|
|
1691
|
-
}) }), /* @__PURE__ */ jsxs(Menu.Dropdown, { children: [
|
|
1692
|
-
/* @__PURE__ */ jsx(Menu.Item, {
|
|
1693
|
-
onClick: handleDownloadPng,
|
|
1694
|
-
children: "PNG"
|
|
1695
|
-
}),
|
|
1696
|
-
/* @__PURE__ */ jsx(Menu.Item, {
|
|
1697
|
-
onClick: handleDownloadSvg,
|
|
1698
|
-
children: "SVG"
|
|
1699
|
-
}),
|
|
1700
|
-
/* @__PURE__ */ jsx(Menu.Item, {
|
|
1701
|
-
onClick: handleDownloadPdf,
|
|
1702
|
-
children: "PDF"
|
|
1703
|
-
})
|
|
1704
|
-
] })]
|
|
1705
|
-
}),
|
|
1706
|
-
menuItems?.map((menuItem) => /* @__PURE__ */ jsx(ControlButton, {
|
|
1707
|
-
onClick: menuItem.onClick,
|
|
1708
|
-
title: menuItem.label,
|
|
1709
|
-
children: menuItem.icon
|
|
1710
|
-
}))
|
|
1711
|
-
]
|
|
1712
|
-
})] })
|
|
1669
|
+
}), /* @__PURE__ */ jsx(GraphControls, {
|
|
1670
|
+
isPreview,
|
|
1671
|
+
onFitView: fitViewWithEdges,
|
|
1672
|
+
onDownloadPng: downloadPng,
|
|
1673
|
+
onDownloadSvg: downloadSvg,
|
|
1674
|
+
onDownloadPdf: downloadPdf,
|
|
1675
|
+
menuItems
|
|
1676
|
+
})]
|
|
1713
1677
|
})
|
|
1714
1678
|
})] });
|
|
1715
1679
|
};
|