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>

Reply via email to