From e07463c66ccafb233c613d37570e9df5f72ac775 Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Fri, 18 Oct 2024 19:05:03 +0300 Subject: [PATCH 01/16] Add TypeScript to debugger react-scripts does not support typescript 5, so it uses 4 --- .../egraph-debug-template/package.json | 6 ++++- .../egraph-debug-template/tsconfig.json | 26 +++++++++++++++++++ .../cubesql/src/compile/rewrite/rewriter.rs | 4 +++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 rust/cubesql/cubesql/egraph-debug-template/tsconfig.json diff --git a/rust/cubesql/cubesql/egraph-debug-template/package.json b/rust/cubesql/cubesql/egraph-debug-template/package.json index 52fd01baa8bcf..69cb3e9ab0221 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/package.json +++ b/rust/cubesql/cubesql/egraph-debug-template/package.json @@ -16,8 +16,12 @@ "eject": "react-scripts eject" }, "devDependencies": { + "@types/node": "20.16.12", + "@types/react": "18.0.38", + "@types/react-dom": "18.0.11", "prettier": "3.3.3", - "react-scripts": "5.0.1" + "react-scripts": "5.0.1", + "typescript": "4.9.5" }, "eslintConfig": { "extends": [ diff --git a/rust/cubesql/cubesql/egraph-debug-template/tsconfig.json b/rust/cubesql/cubesql/egraph-debug-template/tsconfig.json new file mode 100644 index 0000000000000..a273b0cfc0e96 --- /dev/null +++ b/rust/cubesql/cubesql/egraph-debug-template/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rewriter.rs b/rust/cubesql/cubesql/src/compile/rewrite/rewriter.rs index a69115d74d7c3..53c15a0022291 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rewriter.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rewriter.rs @@ -181,6 +181,10 @@ fn write_debug_states(runner: &CubeRunner, stage: &str) -> Result<(), CubeError> "egraph-debug-template/package.json", format!("{}/package.json", dir), )?; + fs::copy( + "egraph-debug-template/tsconfig.json", + format!("{}/tsconfig.json", dir), + )?; fs::copy( "egraph-debug-template/src/index.js", format!("{}/src/index.js", dir), From 3e1ac5453de1e49443c67b9c90ace7272b98eeb4 Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Fri, 18 Oct 2024 20:58:49 +0300 Subject: [PATCH 02/16] Fix stroke width spelling --- rust/cubesql/cubesql/egraph-debug-template/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index a52dd170c1098..1f1481783795d 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -46,7 +46,7 @@ const toEdge = (n) => ({ id: `${n.source}->${n.target}`, style: n.source.indexOf(`${n.target}-`) === 0 - ? { stroke: '#f00', 'stroke-width': 10 } + ? { stroke: '#f00', strokeWidth: 10 } : undefined, }); const initialNodes = data.combos From 619e3d2175ec83c750b8e9f9dca7b637ab25e68a Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Fri, 18 Oct 2024 21:00:16 +0300 Subject: [PATCH 03/16] Fix ELK options types According to their own typings, LayoutOptions is a string->string record --- rust/cubesql/cubesql/egraph-debug-template/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index 1f1481783795d..9e4a10820ac00 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -66,8 +66,8 @@ function layout( ) { const defaultOptions = { 'elk.algorithm': 'layered', - 'elk.layered.spacing.nodeNodeBetweenLayers': 100, - 'elk.spacing.nodeNode': 80, + 'elk.layered.spacing.nodeNodeBetweenLayers': '100', + 'elk.spacing.nodeNode': '80', 'org.eclipse.elk.hierarchyHandling': 'INCLUDE_CHILDREN', 'elk.direction': 'DOWN', }; From f7583fe666d4a8785200cd15d1f4ba8f32489331 Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Fri, 18 Oct 2024 21:16:10 +0300 Subject: [PATCH 04/16] Switch to ElkExtendedEdge --- rust/cubesql/cubesql/egraph-debug-template/src/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index 9e4a10820ac00..5c1032a21dfbc 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -97,11 +97,18 @@ function layout( }), ); + // Primitive edges are deprecated in ELK, so we should use ElkExtendedEdge, that use arrays, essentially hyperedges + const elkEdges = edges.map((edge) => ({ + id: edge.id, + sources: [edge.source], + targets: [edge.target], + })); + const graph = { id: 'root', layoutOptions: layoutOptions, children: Object.keys(groupNodes).map((key) => groupNodes[key]), - edges: edges, + edges: elkEdges, }; const elk = new ELK(); From 195af9fffe9dad60b182736ded059e6fa8d346aa Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Fri, 18 Oct 2024 21:16:47 +0300 Subject: [PATCH 05/16] Use property shorthand --- rust/cubesql/cubesql/egraph-debug-template/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index 5c1032a21dfbc..55be86759e3df 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -106,7 +106,7 @@ function layout( const graph = { id: 'root', - layoutOptions: layoutOptions, + layoutOptions, children: Object.keys(groupNodes).map((key) => groupNodes[key]), edges: elkEdges, }; From 7a9e09b546f7dc4dd069e651c9da1913f6b10eae Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Fri, 18 Oct 2024 21:19:27 +0300 Subject: [PATCH 06/16] Extract JSON clone to function --- .../cubesql/egraph-debug-template/src/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index 55be86759e3df..c6f72bb6aabec 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -250,16 +250,20 @@ const ChildrenNode = ); }; +function jsonClone(t) { + return JSON.parse(JSON.stringify(t)); +} + const LayoutFlow = () => { const [{ preNodes, preEdges }, setPreNodesEdges] = useState({ preNodes: initialNodes, preEdges: initialEdges, }); const [nodes, setNodes, onNodesChange] = useNodesState( - JSON.parse(JSON.stringify(initialNodes)), + jsonClone(initialNodes), ); const [edges, setEdges, onEdgesChange] = useEdgesState( - JSON.parse(JSON.stringify(initialEdges)), + jsonClone(initialEdges), ); const [stateIdx, setStateIdx] = useState(0); const { fitView } = useReactFlow(); @@ -370,8 +374,8 @@ const LayoutFlow = () => { useEffect(() => { layout( {}, - JSON.parse(JSON.stringify(preNodes)), - JSON.parse(JSON.stringify(preEdges)), + jsonClone(preNodes), + jsonClone(preEdges), setNodes, setEdges, fitView, From 73987316e44b105ccec27277c9a29a2036276872 Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Fri, 18 Oct 2024 21:19:55 +0300 Subject: [PATCH 07/16] Check root element presence --- rust/cubesql/cubesql/egraph-debug-template/src/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index c6f72bb6aabec..ea71b5101cba6 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -470,5 +470,8 @@ function rootComponent() { } const rootElement = document.getElementById('ui'); +if (rootElement === null) { + throw new Error('Root element not found'); +} const root = createRoot(rootElement); root.render(rootComponent()); From 5cb243fa71025d5a319753e2149f3c50610ccc5e Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Sat, 19 Oct 2024 03:29:31 +0300 Subject: [PATCH 08/16] Make ELK -> ReactFlow conversion recursive --- .../egraph-debug-template/src/index.js | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index ea71b5101cba6..c79a76107fc10 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -111,6 +111,23 @@ function layout( edges: elkEdges, }; + function elk2flow(node, flattenChildren, withStyle) { + node.position = { x: node.x, y: node.y }; + if (withStyle) { + node.style = { + ...node.style, + width: node.width, + height: node.height, + }; + } + flattenChildren.push(node); + (node.children ?? []).forEach((child) => { + // only depth 0 get styles + elk2flow(child, flattenChildren, false); + }); + delete node.children; + } + const elk = new ELK(); return elk.layout(graph).then(({ children }) => { // By mutating the children in-place we saves ourselves from creating a @@ -118,18 +135,7 @@ function layout( const flattenChildren = []; children.forEach((node) => { - node.position = { x: node.x, y: node.y }; - node.style = { - ...node.style, - width: node.width, - height: node.height, - }; - flattenChildren.push(node); - node.children.forEach((child) => { - child.position = { x: child.x, y: child.y }; - flattenChildren.push(child); - }); - delete node.children; + elk2flow(node, flattenChildren, true); }); setNodes(flattenChildren); From 9661dda43233128a8a57c7a1a0d9aad02a07802f Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Tue, 22 Oct 2024 00:49:43 +0300 Subject: [PATCH 09/16] Remove unnecessary withStyle param --- .../egraph-debug-template/src/index.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index c79a76107fc10..1752e0266258d 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -111,19 +111,16 @@ function layout( edges: elkEdges, }; - function elk2flow(node, flattenChildren, withStyle) { + function elk2flow(node, flattenChildren) { node.position = { x: node.x, y: node.y }; - if (withStyle) { - node.style = { - ...node.style, - width: node.width, - height: node.height, - }; - } + node.style = { + ...node.style, + width: node.width, + height: node.height, + }; flattenChildren.push(node); (node.children ?? []).forEach((child) => { - // only depth 0 get styles - elk2flow(child, flattenChildren, false); + elk2flow(child, flattenChildren); }); delete node.children; } @@ -135,7 +132,7 @@ function layout( const flattenChildren = []; children.forEach((node) => { - elk2flow(node, flattenChildren, true); + elk2flow(node, flattenChildren); }); setNodes(flattenChildren); From af6ce44fd43261e5a61ea0041dc38ab2d9875fe9 Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Mon, 21 Oct 2024 22:14:43 +0300 Subject: [PATCH 10/16] Use async function for layout effect --- .../egraph-debug-template/src/index.js | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index 1752e0266258d..c5eb935bf67fd 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -54,7 +54,7 @@ const initialNodes = data.combos .concat(data.nodes.map(toRegularNode)); const initialEdges = data.edges.map(toEdge); -function layout( +async function layout( options, nodes, edges, @@ -126,28 +126,28 @@ function layout( } const elk = new ELK(); - return elk.layout(graph).then(({ children }) => { - // By mutating the children in-place we saves ourselves from creating a - // needless copy of the nodes array. - const flattenChildren = []; + const { children } = await elk.layout(graph); - children.forEach((node) => { - elk2flow(node, flattenChildren); - }); + // By mutating the children in-place we saves ourselves from creating a + // needless copy of the nodes array. + const flattenChildren = []; - setNodes(flattenChildren); - setEdges(edges); - window.requestAnimationFrame(() => { - if (navHistory?.length) { - setTimeout(() => { - zoomTo(fitView, navHistory); - }, 500); - } else { - fitView(); - } - }); - return flattenChildren; + children.forEach((node) => { + elk2flow(node, flattenChildren); + }); + + setNodes(flattenChildren); + setEdges(edges); + window.requestAnimationFrame(() => { + if (navHistory?.length) { + setTimeout(() => { + zoomTo(fitView, navHistory); + }, 500); + } else { + fitView(); + } }); + return flattenChildren; } const highlightColor = 'rgba(170,255,170,0.71)'; From 73218714d661d3ab8a90cc374ad1535de6dc80b8 Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Sat, 19 Oct 2024 03:45:02 +0300 Subject: [PATCH 11/16] Simplify ReactFlow <-> ELK interop --- .../egraph-debug-template/src/index.js | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index c5eb935bf67fd..00290a6d46b82 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -81,21 +81,31 @@ async function layout( }); nodes = nodes.filter((n) => !isHiddenNode(showOnlySelected, navHistory, n)); edges = edges.filter((e) => !isHiddenEdge(showOnlySelected, navHistory, e)); - const groupNodes = nodes - .filter((node) => node.type === 'group') - .map((node) => ({ [node.id]: node })) - .reduce((acc, val) => ({ ...acc, ...val }), {}); - nodes - .filter((node) => node.type !== 'group') - .forEach( - (node) => - (groupNodes[node.parentNode] = { - ...groupNodes[node.parentNode], - children: ( - groupNodes[node.parentNode]?.children || [] - ).concat(node), - }), - ); + + const nodesMap = new Map( + nodes.map((node) => [ + node.id, + { + node, + elkNode: { + id: node.id, + width: node.width ?? undefined, + height: node.height ?? undefined, + children: [], + }, + }, + ]), + ); + + for (const { node, elkNode } of nodesMap.values()) { + if (node.type === 'group') { + continue; + } + if (node.parentNode === undefined) { + return; + } + nodesMap.get(node.parentNode).elkNode.children.push(elkNode); + } // Primitive edges are deprecated in ELK, so we should use ElkExtendedEdge, that use arrays, essentially hyperedges const elkEdges = edges.map((edge) => ({ @@ -107,22 +117,27 @@ async function layout( const graph = { id: 'root', layoutOptions, - children: Object.keys(groupNodes).map((key) => groupNodes[key]), + children: [...nodesMap.values()] + .filter(({ node }) => node.type === 'group') + .map(({ elkNode }) => elkNode), edges: elkEdges, }; - function elk2flow(node, flattenChildren) { - node.position = { x: node.x, y: node.y }; + function elk2flow(elkNode, flatChildren) { + const node = nodesMap.get(elkNode.id).node; + + node.position = { x: elkNode.x, y: elkNode.y }; node.style = { ...node.style, - width: node.width, - height: node.height, + width: elkNode.width, + height: elkNode.height, }; - flattenChildren.push(node); - (node.children ?? []).forEach((child) => { - elk2flow(child, flattenChildren); + node.width = elkNode.width; + node.height = elkNode.height; + flatChildren.push(node); + (elkNode.children ?? []).forEach((child) => { + elk2flow(child, flatChildren); }); - delete node.children; } const elk = new ELK(); @@ -130,13 +145,13 @@ async function layout( // By mutating the children in-place we saves ourselves from creating a // needless copy of the nodes array. - const flattenChildren = []; + const flatChildren = []; - children.forEach((node) => { - elk2flow(node, flattenChildren); + children.forEach((elkNode) => { + elk2flow(elkNode, flatChildren); }); - setNodes(flattenChildren); + setNodes(flatChildren); setEdges(edges); window.requestAnimationFrame(() => { if (navHistory?.length) { @@ -147,7 +162,7 @@ async function layout( fitView(); } }); - return flattenChildren; + return flatChildren; } const highlightColor = 'rgba(170,255,170,0.71)'; From 2e8e4f01170a3d7b53771795e23c5689abda3b6b Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Mon, 21 Oct 2024 22:39:29 +0300 Subject: [PATCH 12/16] Implement cleanup for layout effect This should allow changing nodes multiple times without waiting for every zoom animation --- .../egraph-debug-template/src/index.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index 00290a6d46b82..23d8110cbafce 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -63,6 +63,7 @@ async function layout( fitView, navHistory, showOnlySelected, + abortSignal, ) { const defaultOptions = { 'elk.algorithm': 'layered', @@ -143,6 +144,10 @@ async function layout( const elk = new ELK(); const { children } = await elk.layout(graph); + if (abortSignal.aborted) { + return; + } + // By mutating the children in-place we saves ourselves from creating a // needless copy of the nodes array. const flatChildren = []; @@ -153,9 +158,21 @@ async function layout( setNodes(flatChildren); setEdges(edges); + + if (abortSignal.aborted) { + return; + } window.requestAnimationFrame(() => { + if (abortSignal.aborted) { + return; + } + if (navHistory?.length) { setTimeout(() => { + if (abortSignal.aborted) { + return; + } + zoomTo(fitView, navHistory); }, 500); } else { @@ -390,6 +407,8 @@ const LayoutFlow = () => { ); useEffect(() => { + const ac = new AbortController(); + layout( {}, jsonClone(preNodes), @@ -399,7 +418,10 @@ const LayoutFlow = () => { fitView, navHistory, showOnlySelected, + ac.signal, ); + + return () => ac.abort(); }, [ preNodes, setNodes, From 862ba372f097e17ab357e3fed3dd24725e8364be Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Tue, 22 Oct 2024 17:08:31 +0300 Subject: [PATCH 13/16] Simplify zooming after ELK layout --- .../cubesql/egraph-debug-template/src/index.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index 23d8110cbafce..eb65bf7f838d1 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -162,23 +162,14 @@ async function layout( if (abortSignal.aborted) { return; } - window.requestAnimationFrame(() => { + // TODO investigate why setTimeout is necessary, something related to ReactFlow state and setNodes/setEdges probably + setTimeout(() => { if (abortSignal.aborted) { return; } - if (navHistory?.length) { - setTimeout(() => { - if (abortSignal.aborted) { - return; - } - - zoomTo(fitView, navHistory); - }, 500); - } else { - fitView(); - } - }); + zoomTo(fitView, navHistory); + }, 500); return flatChildren; } From 5aa8021e913b8c1f0f79240b111aa4610333b1c9 Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Tue, 22 Oct 2024 02:34:04 +0300 Subject: [PATCH 14/16] Use empty object instead of undefined for empty styles --- rust/cubesql/cubesql/egraph-debug-template/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.js index eb65bf7f838d1..d1275b37103d4 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.js @@ -47,7 +47,7 @@ const toEdge = (n) => ({ style: n.source.indexOf(`${n.target}-`) === 0 ? { stroke: '#f00', strokeWidth: 10 } - : undefined, + : {}, }); const initialNodes = data.combos .map(toGroupNode) From e8e59f955ac5e432e107d1dcd94655d15e251c33 Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Fri, 18 Oct 2024 20:46:17 +0300 Subject: [PATCH 15/16] Port debugger to TS --- .../egraph-debug-template/package.json | 2 + .../src/{index.js => index.tsx} | 169 +++++++++++++----- .../egraph-debug-template/tsconfig.json | 39 ++-- .../cubesql/src/compile/rewrite/rewriter.rs | 4 +- 4 files changed, 153 insertions(+), 61 deletions(-) rename rust/cubesql/cubesql/egraph-debug-template/src/{index.js => index.tsx} (77%) diff --git a/rust/cubesql/cubesql/egraph-debug-template/package.json b/rust/cubesql/cubesql/egraph-debug-template/package.json index 69cb3e9ab0221..a95d56c60cdd4 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/package.json +++ b/rust/cubesql/cubesql/egraph-debug-template/package.json @@ -11,11 +11,13 @@ "scripts": { "start": "GENERATE_SOURCEMAP=false && react-scripts start", "build": "react-scripts build", + "check": "tsc", "reformat": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": { + "@tsconfig/strictest": "2.0.5", "@types/node": "20.16.12", "@types/react": "18.0.38", "@types/react-dom": "18.0.11", diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.js b/rust/cubesql/cubesql/egraph-debug-template/src/index.tsx similarity index 77% rename from rust/cubesql/cubesql/egraph-debug-template/src/index.js rename to rust/cubesql/cubesql/egraph-debug-template/src/index.tsx index d1275b37103d4..ca83b441370f9 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.js +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.tsx @@ -1,7 +1,7 @@ -import states from './states.json'; import { createRoot } from 'react-dom/client'; import ELK from 'elkjs/lib/elk.bundled.js'; -import React, { useCallback, useState, useEffect, useMemo } from 'react'; +import type { ElkNode, LayoutOptions } from 'elkjs'; +import { useCallback, useState, useEffect, useMemo } from 'react'; import ReactFlow, { ReactFlowProvider, Panel, @@ -11,9 +11,49 @@ import ReactFlow, { Handle, Position, } from 'reactflow'; - +import type { + Edge as ReactFlowEdge, + FitView, + Node as ReactFlowNode, + NodeProps, +} from 'reactflow'; import 'reactflow/dist/style.css'; +import statesData from './states.json'; + +type InputNodeData = { + id: string; + label: string; + comboId: string; +}; +type InputEdgeData = { + source: string; + target: string; +}; +type InputComboData = { + id: string; + label: string; +}; +type StateData = { + nodes: Array; + removedNodes: Array; + edges: Array; + removedEdges: Array; + combos: Array; + removedCombos: Array; + appliedRules: Array; +}; +type InputData = Array; + +type NodeData = { + label: string; +}; +type Node = ReactFlowNode; +type Edge = ReactFlowEdge; + +// TODO proper parsing here +const states = statesData as InputData; + // First is initial state const totalIterations = states.length - 1; const data = { @@ -21,8 +61,11 @@ const data = { edges: states[0].edges, combos: states[0].combos, }; -const sizeByNode = (n) => [60 + n.label.length * 5, 30]; -const toGroupNode = (n) => ({ +const sizeByNode = (n: InputNodeData): [number, number] => [ + 60 + n.label.length * 5, + 30, +]; +const toGroupNode = (n: InputComboData): Node => ({ ...n, type: 'group', data: { label: n.label }, @@ -30,7 +73,7 @@ const toGroupNode = (n) => ({ width: 200, height: 200, }); -const toRegularNode = (n) => ({ +const toRegularNode = (n: InputNodeData): Node => ({ ...n, type: 'default', extent: 'parent', @@ -41,7 +84,7 @@ const toRegularNode = (n) => ({ draggable: false, connectable: false, }); -const toEdge = (n) => ({ +const toEdge = (n: InputEdgeData): Edge => ({ ...n, id: `${n.source}->${n.target}`, style: @@ -55,15 +98,15 @@ const initialNodes = data.combos const initialEdges = data.edges.map(toEdge); async function layout( - options, - nodes, - edges, - setNodes, - setEdges, - fitView, - navHistory, - showOnlySelected, - abortSignal, + options: LayoutOptions, + nodes: Array, + edges: Array, + setNodes: (nodes: Array) => void, + setEdges: (nodes: Array) => void, + fitView: FitView, + navHistory: NavHistoryState, + showOnlySelected: boolean, + abortSignal: AbortSignal, ) { const defaultOptions = { 'elk.algorithm': 'layered', @@ -76,6 +119,12 @@ async function layout( nodes.forEach((n) => { if (n.style && n.style.width && n.style.height) { + if (typeof n.style.width === 'string') { + throw new Error('Unexpeted CSS width'); + } + if (typeof n.style.height === 'string') { + throw new Error('Unexpeted CSS height'); + } n.width = n.style.width; n.height = n.style.height; } @@ -83,6 +132,8 @@ async function layout( nodes = nodes.filter((n) => !isHiddenNode(showOnlySelected, navHistory, n)); edges = edges.filter((e) => !isHiddenEdge(showOnlySelected, navHistory, e)); + type ElkNodeWithChildren = ElkNode & { children: Array }; + const nodesMap = new Map( nodes.map((node) => [ node.id, @@ -93,7 +144,7 @@ async function layout( width: node.width ?? undefined, height: node.height ?? undefined, children: [], - }, + } as ElkNodeWithChildren, }, ]), ); @@ -105,7 +156,8 @@ async function layout( if (node.parentNode === undefined) { return; } - nodesMap.get(node.parentNode).elkNode.children.push(elkNode); + // Safety: we've just inserted every node from nodes to map + nodesMap.get(node.parentNode)!.elkNode.children.push(elkNode); } // Primitive edges are deprecated in ELK, so we should use ElkExtendedEdge, that use arrays, essentially hyperedges @@ -115,7 +167,7 @@ async function layout( targets: [edge.target], })); - const graph = { + const graph: ElkNode = { id: 'root', layoutOptions, children: [...nodesMap.values()] @@ -124,17 +176,24 @@ async function layout( edges: elkEdges, }; - function elk2flow(elkNode, flatChildren) { - const node = nodesMap.get(elkNode.id).node; + function elk2flow(elkNode: ElkNode, flatChildren: Array): void { + const nodePair = nodesMap.get(elkNode.id); + if (nodePair === undefined) { + throw new Error('Unexpected node id from ELK'); + } + const node = nodePair.node; + if (elkNode.x === undefined || elkNode.y === undefined) { + throw new Error('Unexpected position from ELK'); + } node.position = { x: elkNode.x, y: elkNode.y }; node.style = { ...node.style, width: elkNode.width, height: elkNode.height, }; - node.width = elkNode.width; - node.height = elkNode.height; + node.width = elkNode.width ?? null; + node.height = elkNode.height ?? null; flatChildren.push(node); (elkNode.children ?? []).forEach((child) => { elk2flow(child, flatChildren); @@ -150,9 +209,9 @@ async function layout( // By mutating the children in-place we saves ourselves from creating a // needless copy of the nodes array. - const flatChildren = []; + const flatChildren: Array = []; - children.forEach((elkNode) => { + (children ?? []).forEach((elkNode) => { elk2flow(elkNode, flatChildren); }); @@ -176,14 +235,18 @@ async function layout( const highlightColor = 'rgba(170,255,170,0.71)'; const selectColor = 'rgba(170,187,255,0.71)'; -const zoomTo = (fitView, classId) => { +const zoomTo = (fitView: FitView, classId: Array): void => { if (!classId) { return; } fitView({ duration: 600, nodes: classId.map((id) => ({ id: `c${id}` })) }); }; -function isHiddenNode(showOnlySelected, navHistory, n) { +function isHiddenNode( + showOnlySelected: boolean, + navHistory: NavHistoryState, + n: Node, +): boolean { return ( showOnlySelected && navHistory.indexOf( @@ -192,7 +255,11 @@ function isHiddenNode(showOnlySelected, navHistory, n) { ); } -const nodeStyles = (nodes, navHistory, showOnlySelected) => { +const nodeStyles = ( + nodes: Array, + navHistory: NavHistoryState, + showOnlySelected: boolean, +): Array => { return nodes.map((n) => { return { ...n, @@ -208,7 +275,11 @@ const nodeStyles = (nodes, navHistory, showOnlySelected) => { }); }; -function isHiddenEdge(showOnlySelected, navHistory, e) { +function isHiddenEdge( + showOnlySelected: boolean, + navHistory: NavHistoryState, + e: Edge, +): boolean { return ( showOnlySelected && (navHistory.indexOf(e.source.replace(/^(\d+)(-?).*$/, '$1')) === -1 || @@ -216,7 +287,11 @@ function isHiddenEdge(showOnlySelected, navHistory, e) { ); } -const edgeStyles = (edges, navHistory, showOnlySelected) => { +const edgeStyles = ( + edges: Array, + navHistory: NavHistoryState, + showOnlySelected: boolean, +): Array => { return edges.map((e) => { return { ...e, @@ -225,7 +300,7 @@ const edgeStyles = (edges, navHistory, showOnlySelected) => { }); }; -const splitLabel = (label) => { +const splitLabel = (label: string): Array => { const result = ['']; let isDigit = false; for (let i = 0; i < label.length; i++) { @@ -245,8 +320,9 @@ const splitLabel = (label) => { }; const ChildrenNode = - ({ navigate /*, nodes*/ }) => - ({ data: { label } }) => { + ({ navigate /*, nodes*/ }: { navigate: (id: string) => void }) => + (props: NodeProps) => { + const { label } = props.data; return (
@@ -276,12 +352,19 @@ const ChildrenNode = ); }; -function jsonClone(t) { +type PreNodesState = { + preNodes: Array; + preEdges: Array; +}; + +function jsonClone(t: T): T { return JSON.parse(JSON.stringify(t)); } +type NavHistoryState = Array; + const LayoutFlow = () => { - const [{ preNodes, preEdges }, setPreNodesEdges] = useState({ + const [{ preNodes, preEdges }, setPreNodesEdges] = useState({ preNodes: initialNodes, preEdges: initialEdges, }); @@ -295,8 +378,8 @@ const LayoutFlow = () => { const { fitView } = useReactFlow(); const [navigateTo, setNavigateTo] = useState(''); - const [navHistory, setNavHistory] = useState([]); - const [showOnlySelected, setShowOnlySelected] = useState(false); + const [navHistory, setNavHistory] = useState([]); + const [showOnlySelected, setShowOnlySelected] = useState(false); const prevState = () => { if (stateIdx === 0) { @@ -305,7 +388,7 @@ const LayoutFlow = () => { let newNodes = preNodes; let newEdges = preEdges; const toRemove = states[stateIdx]; - let toRemoveNodeIds = toRemove.nodes + let toRemoveNodeIds = (toRemove.nodes as Array<{ id: string }>) .concat(toRemove.combos) .map((n) => n.id); let toRemoveEdgeIds = toRemove.edges.map((n) => toEdge(n).id); @@ -317,14 +400,14 @@ const LayoutFlow = () => { newNodes = newNodes.concat( (toRemove.removedNodes || []).map(toRegularNode), ); - const edgeMap = (toRemove.removedEdges || []) + const edgeMap: Record = (toRemove.removedEdges || []) .map(toEdge) .reduce((acc, val) => ({ ...acc, [val.id]: val }), {}); newEdges = newEdges.concat( Object.keys(edgeMap).map((key) => edgeMap[key]), ); const toHighlight = states[stateIdx - 1]; - const toHighlightNodeIds = toHighlight.nodes + const toHighlightNodeIds = (toHighlight.nodes as Array<{ id: string }>) .concat(toHighlight.combos) .map((n) => n.id); newNodes = newNodes.map((n) => ({ @@ -348,7 +431,7 @@ const LayoutFlow = () => { let newEdges = preEdges; setStateIdx(stateIdx + 1); const toAdd = states[stateIdx + 1]; - let toRemoveNodeIds = toAdd.removedNodes + let toRemoveNodeIds = (toAdd.removedNodes as Array<{ id: string }>) .concat(toAdd.removedCombos) .map((n) => n.id); let toRemoveEdgeIds = toAdd.removedEdges.map((n) => toEdge(n).id); @@ -367,7 +450,7 @@ const LayoutFlow = () => { style: { ...n.style, backgroundColor: highlightColor }, })), ); - const edgeMap = (toAdd.edges || []) + const edgeMap: Record = (toAdd.edges || []) .map(toEdge) .reduce((acc, val) => ({ ...acc, [val.id]: val }), {}); newEdges = newEdges.concat( @@ -378,7 +461,7 @@ const LayoutFlow = () => { }; const navigate = useCallback( - (id) => { + (id: string): void => { zoomTo(fitView, [id]); if (!navHistory.includes(id)) { setNavHistory(navHistory.concat(id)); diff --git a/rust/cubesql/cubesql/egraph-debug-template/tsconfig.json b/rust/cubesql/cubesql/egraph-debug-template/tsconfig.json index a273b0cfc0e96..4f2a30d1734c8 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/tsconfig.json +++ b/rust/cubesql/cubesql/egraph-debug-template/tsconfig.json @@ -1,24 +1,31 @@ { + // multiple base configs is supported in TS 5.0+ + // See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#supporting-multiple-configuration-files-in-extends + // But CRA supports only TS 4 + // To add insult to injury, is does not fail with proper error, it prints "No issues found." instead + + // @tsconfig/create-react-app is a bit broken, it uses moduleResolution=bundler with resolveJsonModule=true, which is incompatible + // TODO make it ["@tsconfig/create-react-app", "@tsconfig/strictest"] after TS bump + "extends": "@tsconfig/strictest", "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, + "allowJs": false, + "checkJs": false, "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, + + // Those are from create-react-app + "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, + "moduleResolution": "NodeNext", + "target": "es2015", + + // Those are from create-react-app + "allowSyntheticDefaultImports": true, + "jsx": "react-jsx", "noEmit": true, - "jsx": "react-jsx" + "resolveJsonModule": true, + + // This adds too much noise when iterating over arrays + "noUncheckedIndexedAccess": false, }, "include": [ "src" diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rewriter.rs b/rust/cubesql/cubesql/src/compile/rewrite/rewriter.rs index 53c15a0022291..70346db658e19 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rewriter.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rewriter.rs @@ -186,8 +186,8 @@ fn write_debug_states(runner: &CubeRunner, stage: &str) -> Result<(), CubeError> format!("{}/tsconfig.json", dir), )?; fs::copy( - "egraph-debug-template/src/index.js", - format!("{}/src/index.js", dir), + "egraph-debug-template/src/index.tsx", + format!("{}/src/index.tsx", dir), )?; let mut states = Vec::new(); From ca083c747b5d56177049208cf30fac4839640f1d Mon Sep 17 00:00:00 2001 From: Mikhail Cheshkov Date: Sat, 19 Oct 2024 04:28:17 +0300 Subject: [PATCH 16/16] Setup web worker for ELK --- .../cubesql/egraph-debug-template/package.json | 3 ++- .../cubesql/egraph-debug-template/src/index.tsx | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/rust/cubesql/cubesql/egraph-debug-template/package.json b/rust/cubesql/cubesql/egraph-debug-template/package.json index a95d56c60cdd4..60c0f901087df 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/package.json +++ b/rust/cubesql/cubesql/egraph-debug-template/package.json @@ -23,7 +23,8 @@ "@types/react-dom": "18.0.11", "prettier": "3.3.3", "react-scripts": "5.0.1", - "typescript": "4.9.5" + "typescript": "4.9.5", + "web-worker": "1.3.0" }, "eslintConfig": { "extends": [ diff --git a/rust/cubesql/cubesql/egraph-debug-template/src/index.tsx b/rust/cubesql/cubesql/egraph-debug-template/src/index.tsx index ca83b441370f9..641a25369371c 100644 --- a/rust/cubesql/cubesql/egraph-debug-template/src/index.tsx +++ b/rust/cubesql/cubesql/egraph-debug-template/src/index.tsx @@ -1,5 +1,5 @@ import { createRoot } from 'react-dom/client'; -import ELK from 'elkjs/lib/elk.bundled.js'; +import ELK from 'elkjs'; import type { ElkNode, LayoutOptions } from 'elkjs'; import { useCallback, useState, useEffect, useMemo } from 'react'; import ReactFlow, { @@ -97,6 +97,18 @@ const initialNodes = data.combos .concat(data.nodes.map(toRegularNode)); const initialEdges = data.edges.map(toEdge); +const elk = new ELK({ + workerFactory: function (_url) { + // TODO something is broken with bundling and web-worker + return new Worker( + new URL( + '../node_modules/elkjs/lib/elk-worker.min.js', + import.meta.url, + ), + ); + }, +}); + async function layout( options: LayoutOptions, nodes: Array, @@ -200,7 +212,8 @@ async function layout( }); } - const elk = new ELK(); + // TODO add throbber while waiting for layout + // TODO add queue to be able to cancel request before sending it to worker const { children } = await elk.layout(graph); if (abortSignal.aborted) {