This is an automated email from the ASF dual-hosted git repository. skrawcz pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/hamilton.git
commit 28a58be6c1c470a59f959913caa188bc57886f02 Author: Stefan Krawczyk <[email protected]> AuthorDate: Sun Feb 22 22:50:06 2026 -0800 Fix infinite rerender loop and React warnings in DAG visualization - Add throttling and debouncing to NodeDimensionsSetter to prevent infinite update loops - Remove triggerRerender() from onNodesChange handler - Fix node click behavior to properly toggle console and highlight nodes - Fix onNodeMouseLeave calling wrong function (was calling onNodeGroupEnter) - Fix setCurrentFocusGroup to create new searchParams instead of mutating - Fix React 19 key spreading warnings in syntax highlighting components - Fix unsupported style properties (word-break → wordBreak, white-space → whiteSpace) - Add missing Fragment keys in list rendering - Move telemetry file from /data to /tmp to avoid permission issues --- ui/backend/server/trackingserver_base/apps.py | 7 +- .../src/components/dashboard/Visualize/DAGViz.tsx | 129 ++++++++++++++------- .../components/dashboard/Visualize/VizConsole.tsx | 39 ++++--- 3 files changed, 111 insertions(+), 64 deletions(-) diff --git a/ui/backend/server/trackingserver_base/apps.py b/ui/backend/server/trackingserver_base/apps.py index ef145477..d6c2ccd1 100644 --- a/ui/backend/server/trackingserver_base/apps.py +++ b/ui/backend/server/trackingserver_base/apps.py @@ -53,12 +53,13 @@ class TrackingServerConfig(AppConfig): def enable_telemetry(self): if is_telemetry_enabled() and settings.HAMILTON_ENV == "local": - if not os.path.exists("/data/telemetry.txt"): + telemetry_file = "/tmp/hamilton-telemetry.txt" + if not os.path.exists(telemetry_file): telemetry_key = str(uuid.uuid4()) - with open("/data/telemetry.txt", "w") as f: + with open(telemetry_file, "w") as f: f.write(telemetry_key) else: - with open("/data/telemetry.txt", "r") as f: + with open(telemetry_file, "r") as f: telemetry_key = f.read().strip() send_event_json(create_server_event_json(telemetry_key)) diff --git a/ui/frontend/src/components/dashboard/Visualize/DAGViz.tsx b/ui/frontend/src/components/dashboard/Visualize/DAGViz.tsx index 8e08bebc..c0233cc1 100644 --- a/ui/frontend/src/components/dashboard/Visualize/DAGViz.tsx +++ b/ui/frontend/src/components/dashboard/Visualize/DAGViz.tsx @@ -55,6 +55,7 @@ import { getBezierPath, useEdgesState, useNodesState, + useReactFlow, useStore, } from "reactflow"; @@ -730,21 +731,25 @@ export const CodeView: React.FC<{ const styleToRender = { ...style, backgroundColor: "transparent", - "word-break": "break-all", - "white-space": "pre-wrap", + wordBreak: "break-all", + whiteSpace: "pre-wrap", }; className += ""; return ( <pre className={className} style={styleToRender}> - {tokens.map((line, i) => ( - // eslint-disable-next-line react/jsx-key - <div {...getLineProps({ line, key: i })}> - {line.map((token, key) => ( - // eslint-disable-next-line react/jsx-key - <span hidden={false} {...getTokenProps({ token, key })} /> - ))} - </div> - ))} + {tokens.map((line, i) => { + const { key: _lineKey, ...lineProps } = getLineProps({ line, key: i }); + return ( + <div key={i} {...lineProps}> + {line.map((token, key) => { + const { key: _tokenKey, ...tokenProps } = getTokenProps({ token, key }); + return ( + <span key={key} hidden={false} {...tokenProps} /> + ); + })} + </div> + ); + })} </pre> ); }} @@ -1122,34 +1127,55 @@ const NodeDimensionsSetter = (props: { const nodesWithData = useStore((state) => { return Array.from(state.nodeInternals.values()); }); + + const lastUpdateRef = useRef(0); + const timeoutRef = useRef<NodeJS.Timeout | null>(null); + useLayoutEffect(() => { - let anyChanges = false; - const newNodeDimensions = new Map< - string, - { height: number; width: number } - >(); - nodesWithData.forEach((node) => { - // if (node.type === "group") { - // return; - // } - const oldDimensions = props.nodeDimensions.get(node.id); - if ( - oldDimensions?.height !== node.height || - oldDimensions?.width !== node.width - ) { - anyChanges = true; - } + // Throttle updates to at most once every 500ms to prevent infinite loops + const now = Date.now(); + if (now - lastUpdateRef.current < 500) { + return; + } - newNodeDimensions.set(node.id, { - height: node.height || 0, - width: node.width || 0, - }); - }); - if (anyChanges) { - props.setNodeDimensions(newNodeDimensions); + // Debounce to avoid multiple rapid updates + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); } + + timeoutRef.current = setTimeout(() => { + let anyChanges = false; + const newNodeDimensions = new Map<string, { height: number; width: number }>(); + + nodesWithData.forEach((node) => { + const oldDimensions = props.nodeDimensions.get(node.id); + if ( + oldDimensions?.height !== node.height || + oldDimensions?.width !== node.width + ) { + anyChanges = true; + } + + newNodeDimensions.set(node.id, { + height: node.height || 0, + width: node.width || 0, + }); + }); + + if (anyChanges) { + lastUpdateRef.current = Date.now(); + props.setNodeDimensions(newNodeDimensions); + } + }, 100); // 100ms debounce + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.nodeDimensions]); + }, [nodesWithData]); + return <></>; }; @@ -1279,11 +1305,10 @@ export const VisualizeDAG: React.FC<DAGVisualizeProps> = (props) => { const setCurrentFocusGroup = (focusGroup: string | undefined) => { if (focusGroup) { - searchParams.set("focus", JSON.stringify({ group: focusGroup })); + setSearchParams({ focus: JSON.stringify({ group: focusGroup }) }); } else { - searchParams.delete("focus"); + setSearchParams({}); } - setSearchParams(searchParams); }; const currentFocusNodesRef = useRef<Map<string, DAGNode>>(new Map()); @@ -1496,10 +1521,7 @@ export const VisualizeDAG: React.FC<DAGVisualizeProps> = (props) => { nodesDraggable={false} nodes={nodes} edges={edges} - onNodesChange={(changes: NodeChange[]) => { - onNodesChange(changes); - triggerRerender(); - }} + onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} fitView fitViewOptions={{ @@ -1515,14 +1537,33 @@ export const VisualizeDAG: React.FC<DAGVisualizeProps> = (props) => { elementsSelectable={false} proOptions={{ hideAttribution: true }} onNodeClick={(event, node) => { - props.nodeInteractions?.onNodeGroupClick?.(node.data.nodes); + if (props.nodeInteractions?.onNodeGroupClick) { + props.nodeInteractions.onNodeGroupClick(node.data.nodes); + } else if (props.enableVizConsole) { + // Default behavior: toggle console visibility and set focus + const focusedNode = node.data.nodes[0]; + if (focusedNode) { + const currentFocus = currentFocusParamsRaw ? JSON.parse(currentFocusParamsRaw) : {}; + const isAlreadyFocused = currentFocus.node === focusedNode.name; + + if (isAlreadyFocused) { + // Toggle off: clear focus and hide console + setSearchParams({}); + setVizConsoleVisible(false); + } else { + // Toggle on: set focus and show console + setSearchParams({ focus: JSON.stringify({ node: focusedNode.name }) }); + setVizConsoleVisible(true); + } + } + } event.preventDefault(); }} onNodeMouseEnter={(event, node) => { props.nodeInteractions?.onNodeGroupEnter?.(node.data.nodes); }} onNodeMouseLeave={(event, node) => { - props.nodeInteractions?.onNodeGroupEnter?.(node.data.nodes); + props.nodeInteractions?.onNodeGroupLeave?.(node.data.nodes); }} > <Panel position={"bottom-left"}> diff --git a/ui/frontend/src/components/dashboard/Visualize/VizConsole.tsx b/ui/frontend/src/components/dashboard/Visualize/VizConsole.tsx index b0991db8..a8f23be8 100644 --- a/ui/frontend/src/components/dashboard/Visualize/VizConsole.tsx +++ b/ui/frontend/src/components/dashboard/Visualize/VizConsole.tsx @@ -68,12 +68,12 @@ export const NodeVizConsole = (props: NodeVizConsoleProps) => { > <Dialog.Panel className="pointer-events-auto max-w-6xl w-full"> <div className="flex h-full flex-col overflow-y-scroll bg-gray-800/80 py-6 shadow-xl z-5w-full"> - {Array.from(uniqueCodeMap.entries()).map(([, items]) => { + {Array.from(uniqueCodeMap.entries()).map(([codeArtifactName, items]) => { const dagIndices = new Set( items.map((node) => node.dagIndex) ); return ( - <> + <Fragment key={codeArtifactName}> <div className="px-4 sm:px-6"> <div className="flex flex-row gap-2"> <div className="text-base font-semibold leading-6 text-white break-words w-full flex-row flex-wrap gap-2"> @@ -126,8 +126,8 @@ export const NodeVizConsole = (props: NodeVizConsoleProps) => { const styleToRender = { ...style, backgroundColor: "transparent", - "word-break": "break-all", - "white-space": "pre-wrap", + wordBreak: "break-all", + whiteSpace: "pre-wrap", }; className += ""; return ( @@ -135,18 +135,23 @@ export const NodeVizConsole = (props: NodeVizConsoleProps) => { className={className} style={styleToRender} > - {tokens.map((line, i) => ( - // eslint-disable-next-line react/jsx-key - <div {...getLineProps({ line, key: i })}> - {line.map((token, key) => ( - // eslint-disable-next-line react/jsx-key - <span - hidden={false} - {...getTokenProps({ token, key })} - /> - ))} - </div> - ))} + {tokens.map((line, i) => { + const { key: _lineKey, ...lineProps } = getLineProps({ line, key: i }); + return ( + <div key={i} {...lineProps}> + {line.map((token, key) => { + const { key: _tokenKey, ...tokenProps } = getTokenProps({ token, key }); + return ( + <span + key={key} + hidden={false} + {...tokenProps} + /> + ); + })} + </div> + ); + })} </pre> ); }} @@ -158,7 +163,7 @@ export const NodeVizConsole = (props: NodeVizConsoleProps) => { <div className="w-full border-t border-gray-300" /> </div> </div> - </> + </Fragment> ); })} </div>
