@ocelescope/resources 0.1.2 → 0.5.0

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