From e2c5cad8a3eb47fbf311b3c4476d777f0344d165 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 22 Mar 2026 15:01:35 -0500 Subject: [PATCH] fix(rack-planner): resolve infinite re-render loop in ConnectionLayer and add null-safety for VLAN tooltips --- .../src/components/rack/ConnectionLayer.tsx | 12 +++++-- client/src/components/rack/ModuleBlock.tsx | 2 +- client/src/components/rack/RackPlanner.tsx | 34 ++++++++++--------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/client/src/components/rack/ConnectionLayer.tsx b/client/src/components/rack/ConnectionLayer.tsx index 5cd3950..0ef04d2 100644 --- a/client/src/components/rack/ConnectionLayer.tsx +++ b/client/src/components/rack/ConnectionLayer.tsx @@ -40,11 +40,17 @@ export function ConnectionLayer() { // Also re-calculate if the user scrolls (though ideally lines are pinned to the canvas) // Actually, if SVG is INSIDE the scrollable container, we don't need scroll adjustment. - // We'll use a MutationObserver to detect DOM changes (like modules being added/moved) - const observer = new MutationObserver(updateCoords); + // Use a MutationObserver to detect DOM changes (like modules being added/moved) + const observer = new MutationObserver(() => { + // Small debounce or check if it was our OWN SVG that changed + updateCoords(); + }); + const canvas = document.querySelector('.rack-planner-canvas'); if (canvas) { - observer.observe(canvas, { childList: true, subtree: true, attributes: true }); + // DO NOT observe the entire subtree with attributes if it includes the ConnectionLayer + // Instead, just watch for module layout changes + observer.observe(canvas, { childList: true, subtree: true }); } return () => { diff --git a/client/src/components/rack/ModuleBlock.tsx b/client/src/components/rack/ModuleBlock.tsx index cbc5437..65d9334 100644 --- a/client/src/components/rack/ModuleBlock.tsx +++ b/client/src/components/rack/ModuleBlock.tsx @@ -184,7 +184,7 @@ export function ModuleBlock({ module }: ModuleBlockProps) { onClick={(e) => handlePortClick(e, port.id)} aria-label={`Port ${port.portNumber}`} title={`Port ${port.portNumber}${port.label ? ` ยท ${port.label}` : ''}${ - hasVlan ? ` (VLAN ${port.vlans.map((v) => v.vlan.vlanId).join(',')})` : '' + hasVlan ? ` (VLAN ${port.vlans.map((v) => v.vlan?.vlanId).filter(Boolean).join(',')})` : '' }\nShift+Click for settings`} style={{ backgroundColor: vlanColor, borderColor: 'rgba(0,0,0,0.2)' }} className={cn( diff --git a/client/src/components/rack/RackPlanner.tsx b/client/src/components/rack/RackPlanner.tsx index 900e968..02a1738 100644 --- a/client/src/components/rack/RackPlanner.tsx +++ b/client/src/components/rack/RackPlanner.tsx @@ -258,22 +258,24 @@ export function RackPlanner() { ) : ( - -
- {racks.map((rack) => ( - - ))} - -
-
+ <> + +
+ {racks.map((rack) => ( + + ))} +
+
+ + )}