@ocelescope/resources 0.0.0 → 0.1.0

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