This is an automated email from the ASF dual-hosted git repository. skrawcz pushed a commit to branch stefan/replace-elkjs in repository https://gitbox.apache.org/repos/asf/burr.git
commit d77ee5847e877e1e42eed199e63e5e815107f260 Author: Stefan Krawczyk <[email protected]> AuthorDate: Sun Oct 5 09:24:30 2025 -0700 Removes ELKJS and adds DAGRE DAGRE is MIT licensed and compatible with apache 2.0 Validated that the UI works. --- telemetry/ui/package-lock.json | 195 +++------------------ telemetry/ui/package.json | 3 +- .../ui/src/components/routes/app/GraphView.tsx | 126 ++++++------- 3 files changed, 86 insertions(+), 238 deletions(-) diff --git a/telemetry/ui/package-lock.json b/telemetry/ui/package-lock.json index d830dce4..b71fbefc 100644 --- a/telemetry/ui/package-lock.json +++ b/telemetry/ui/package-lock.json @@ -24,7 +24,7 @@ "@types/react-syntax-highlighter": "^15.5.11", "@uiw/react-json-view": "^2.0.0-alpha.12", "clsx": "^2.1.0", - "elkjs": "^0.9.1", + "dagre": "^0.8.5", "fuse.js": "^7.0.0", "heroicons": "^2.1.1", "react": "^18.2.0", @@ -43,6 +43,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@types/dagre": "^0.7.52", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "eslint": "^8.56.0", @@ -5563,150 +5564,6 @@ "integrity": "sha512-/0hWQfiaD5//LvGNgc8PjvyqV50vGK0cADYzaoOOGN8fxzBn3iAiaq3S0tCRnFBldq0LVveLcxCTi41ZoYgAgg==", "peer": true }, - "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.14.tgz", - "integrity": "sha512-bsxbSAUodM1cjYeA4o6y7sp9wslvwjSkWw57t8DtC8Zig8aG8V6r+Yc05/9mDzLKcybb6EN85k1rJDnMKBd9Gw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.14.tgz", - "integrity": "sha512-cC9/I+0+SK5L1k9J8CInahduTVWGMXhQoXFeNvF0uNs3Bt1Ub0Azb8JzTU9vNCr0hnaMqiWu/Z0S1hfKc3+dww==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.14.tgz", - "integrity": "sha512-RMLOdA2NU4O7w1PQ3Z9ft3PxD6Htl4uB2TJpocm+4jcllHySPkFaUIFacQ3Jekcg6w+LBaFvjSPthZHiPmiAUg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.14.tgz", - "integrity": "sha512-WgLOA4hT9EIP7jhlkPnvz49iSOMdZgDJVvbpb8WWzJv5wBD07M2wdJXLkDYIpZmCFfo/wPqFsFR4JS4V9KkQ2A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.14.tgz", - "integrity": "sha512-lbn7svjUps1kmCettV/R9oAvEW+eUI0lo0LJNFOXoQM5NGNxloAyFRNByYeZKL3+1bF5YE0h0irIJfzXBq9Y6w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.14.tgz", - "integrity": "sha512-7TcQCvLQ/hKfQRgjxMN4TZ2BRB0P7HwrGAYL+p+m3u3XcKTraUFerVbV3jkNZNwDeQDa8zdxkKkw2els/S5onQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.14.tgz", - "integrity": "sha512-8i0Ou5XjTLEje0oj0JiI0Xo9L/93ghFtAUYZ24jARSeTMXLUx8yFIdhS55mTExq5Tj4/dC2fJuaT4e3ySvXU1A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.14.tgz", - "integrity": "sha512-2u2XcSaDEOj+96eXpyjHjtVPLhkAFw2nlaz83EPeuK4obF+HmtDJHqgR1dZB7Gb6V/d55FL26/lYVd0TwMgcOQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.14", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.14.tgz", - "integrity": "sha512-MZom+OvZ1NZxuRovKt1ApevjiUJTcU2PmdJKL66xUPaJeRywnbGGRWUlaAOwunD6dX+pm83vj979NTC8QXjGWg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -7641,6 +7498,13 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/dagre": { + "version": "0.7.53", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz", + "integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -11391,6 +11255,16 @@ "node": ">=12" } }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -11924,11 +11798,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.673.tgz", "integrity": "sha512-zjqzx4N7xGdl5468G+vcgzDhaHkaYgVcf9MqgexcTqsl2UHSCmOj/Bi3HAprg4BZCpC7HyD8a6nZl6QAZf72gw==" }, - "node_modules/elkjs": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.1.tgz", - "integrity": "sha512-JWKDyqAdltuUcyxaECtYG6H4sqysXSLeoXuGUBfRNESMTkj+w+qdb0jya8Z/WI0jVd03WQtCGhS6FOFtlhD5FQ==" - }, "node_modules/emittery": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", @@ -14246,16 +14115,6 @@ "node": ">= 8.0.0" } }, - "node_modules/fuse/node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~6.19.2" - } - }, "node_modules/fuse/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -14712,6 +14571,15 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/graphql": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", @@ -26391,13 +26259,6 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "optional": true, - "peer": true - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/telemetry/ui/package.json b/telemetry/ui/package.json index aaf64e6d..1c492bf3 100644 --- a/telemetry/ui/package.json +++ b/telemetry/ui/package.json @@ -19,7 +19,7 @@ "@types/react-syntax-highlighter": "^15.5.11", "@uiw/react-json-view": "^2.0.0-alpha.12", "clsx": "^2.1.0", - "elkjs": "^0.9.1", + "dagre": "^0.8.5", "fuse.js": "^7.0.0", "heroicons": "^2.1.1", "react": "^18.2.0", @@ -67,6 +67,7 @@ ] }, "devDependencies": { + "@types/dagre": "^0.7.52", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "eslint": "^8.56.0", diff --git a/telemetry/ui/src/components/routes/app/GraphView.tsx b/telemetry/ui/src/components/routes/app/GraphView.tsx index 0de181d2..ce85b823 100644 --- a/telemetry/ui/src/components/routes/app/GraphView.tsx +++ b/telemetry/ui/src/components/routes/app/GraphView.tsx @@ -19,7 +19,7 @@ import { ActionModel, ApplicationModel, Step } from '../../../api'; -import ELK from 'elkjs/lib/elk.bundled.js'; +import dagre from 'dagre'; import React, { createContext, useCallback, useLayoutEffect, useRef, useState } from 'react'; import ReactFlow, { BaseEdge, @@ -39,16 +39,14 @@ import { backgroundColorsForIndex } from './AppView'; import { getActionStatus } from '../../../utils'; import { getSmartEdge } from '@tisoap/react-flow-smart-edge'; -const elk = new ELK(); +const dagreGraph = new dagre.graphlib.Graph(); -const elkOptions = { - 'elk.algorithm': 'layered', - 'elk.layered.spacing.nodeNodeBetweenLayers': '100', - 'elk.spacing.nodeNode': '80', - 'org.eclipse.elk.alg.layered.options.CycleBreakingStrategy': 'GREEDY', - 'org.eclipse.elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', - // 'org.eclipse.elk.layered.feedbackEdges': 'true', - 'org.eclipse.elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP' +const dagreOptions = { + rankdir: 'TB', // Top to bottom layout (equivalent to ELK's UP direction) + nodesep: 80, // Node separation (equivalent to elk.spacing.nodeNode) + ranksep: 100, // Rank separation (equivalent to elk.layered.spacing.nodeNodeBetweenLayers) + marginx: 20, + marginy: 20 }; type ActionNodeData = { @@ -200,68 +198,56 @@ const getLayoutedElements = ( edges: EdgeType[], options: { [key: string]: string } = {} ) => { - const isHorizontal = options?.['elk.direction'] === 'RIGHT'; - const nodeNameMap = nodes.reduce( - (acc, node) => { - acc[node.id] = node; - return acc; - }, - {} as { [key: string]: NodeType } - ); - const edgeNameMap = edges.reduce( - (acc, edge) => { - acc[edge.id] = edge; - return acc; - }, - {} as { [key: string]: EdgeType } - ); - const graph = { - id: 'root', - layoutOptions: options, - children: nodes.map((node) => ({ + const isHorizontal = options?.['direction'] === 'LR'; + const direction = isHorizontal ? 'LR' : 'TB'; + + // Configure dagre graph + dagreGraph.setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ + ...dagreOptions, + rankdir: direction + }); + + // Add nodes to dagre graph + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { + width: 150, + height: 100 + }); + }); + + // Add edges to dagre graph + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + // Calculate layout + dagre.layout(dagreGraph); + + // Apply layout to nodes + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + return { ...node, - // Adjust the target and source handle positions based on the layout - // direction. targetPosition: isHorizontal ? 'left' : 'top', sourcePosition: isHorizontal ? 'right' : 'bottom', + position: { + x: nodeWithPosition.x - 75, // Center the node (width/2) + y: nodeWithPosition.y - 50 // Center the node (height/2) + } + }; + }); - // Hardcode a width and height for elk to use when layouting. - width: 150, - height: 100 - })), - edges: edges.map((edge) => { - return { - ...edge, - sources: [edge.source], - targets: [edge.target] - }; - }) - }; - return elk.layout(graph).then((layoutedGraph) => ({ - nodes: (layoutedGraph.children || []).map((node) => { - const originalNode = nodeNameMap[node.id]; - return { - ...originalNode, - position: { - x: node.x as number, - y: node.y as number - } - }; - }), - edges: (layoutedGraph?.edges || []).map((edge) => { - return { - ...edge, - markerEnd: { type: MarkerType.Arrow, width: 20, height: 20 }, - source: edge.sources[0], - target: edge.targets[0], - data: { - from: edge.sources[0], - to: edge.targets[0], - condition: edgeNameMap[edge.id].data.condition - } - }; - }) + // Apply layout to edges + const layoutedEdges = edges.map((edge) => ({ + ...edge, + markerEnd: { type: MarkerType.Arrow, width: 20, height: 20 } })); + + return Promise.resolve({ + nodes: layoutedNodes, + edges: layoutedEdges + }); }; const convertApplicationToGraph = (stateMachine: ApplicationModel): [NodeType[], EdgeType[]] => { @@ -341,8 +327,8 @@ export const _Graph = (props: { const { fitView } = useReactFlow(); const onLayout = useCallback( - ({ direction = 'UP', useInitialNodes = false }): void => { - const opts = { 'elk.direction': direction, ...elkOptions }; + ({ direction = 'TB', useInitialNodes = false }): void => { + const opts = { direction }; const ns = useInitialNodes ? initialNodes : nodes; const es = useInitialNodes ? initialEdges : edges; @@ -357,7 +343,7 @@ export const _Graph = (props: { ); useLayoutEffect(() => { - onLayout({ direction: 'DOWN', useInitialNodes: true }); + onLayout({ direction: 'TB', useInitialNodes: true }); }, []); return (
