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 (

Reply via email to