From f156d9dd95d981f47b2fe05ba8aa418b5238fb04 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Sun, 8 Mar 2026 13:47:30 +0100 Subject: [PATCH] fix: edges appear immediately + sync virtual link with LXC parent_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Normalize stub handle IDs (top-t/bottom-t → top/bottom) in canvasStore.onConnect so React Flow can locate the handle and render edges without save+reload - Also normalize on save in App.tsx as a safety net for persisted handle IDs - Auto-create virtual edge (LXC top → Proxmox bottom) when parent_id is set via the node edit modal, and remove it when parent_id is cleared/changed - Auto-set LXC/VM parent_id when a virtual edge is drawn to/from a Proxmox node --- frontend/src/App.tsx | 47 ++++++++++++++++++++++++++---- frontend/src/stores/canvasStore.ts | 6 ++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index feb888c..0283974 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -67,8 +67,10 @@ export default function App() { speed: e.data?.speed ?? null, custom_color: e.data?.custom_color ?? null, path_style: e.data?.path_style ?? null, - source_handle: e.sourceHandle ?? null, - target_handle: e.targetHandle ?? null, + // Normalize stub handle IDs: "top-t" / "bottom-t" are invisible target stubs; + // map them back to their canonical source handle ID so reload works correctly. + source_handle: e.sourceHandle === 'top-t' ? 'top' : e.sourceHandle === 'bottom-t' ? 'bottom' : (e.sourceHandle ?? null), + target_handle: e.targetHandle === 'top-t' ? 'top' : e.targetHandle === 'bottom-t' ? 'bottom' : (e.targetHandle ?? null), })) await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport: {} }) markSaved() @@ -162,13 +164,36 @@ export default function App() { const handleUpdateNode = useCallback((data: Partial) => { if (!editNodeId) return + const existingNode = nodes.find((n) => n.id === editNodeId) updateNode(editNodeId, data) // If proxmox container_mode changed, apply structural changes (children parentId, node dimensions) if (data.type === 'proxmox' && typeof data.container_mode === 'boolean') { setProxmoxContainerMode(editNodeId, data.container_mode) } + // Sync virtual edge when parent_id changes on an LXC/VM node + const nodeType = data.type ?? existingNode?.data.type + if ((nodeType === 'lxc' || nodeType === 'vm') && 'parent_id' in data) { + const oldParentId = existingNode?.data.parent_id ?? null + const newParentId = data.parent_id ?? null + if (oldParentId !== newParentId) { + // Remove any existing virtual edge between child and old parent + if (oldParentId) { + const oldEdge = edges.find((e) => + e.data?.type === 'virtual' && + ((e.source === editNodeId && e.target === oldParentId) || + (e.source === oldParentId && e.target === editNodeId)) + ) + if (oldEdge) deleteEdge(oldEdge.id) + } + // Create new virtual edge: LXC top → Proxmox bottom + if (newParentId) { + // Pass type as extra field — canvasStore.onConnect casts to Connection & Partial + onConnect({ source: editNodeId, sourceHandle: 'top', target: newParentId, targetHandle: 'bottom', type: 'virtual' } as unknown as Connection) + } + } + } setEditNodeId(null) - }, [editNodeId, updateNode, setProxmoxContainerMode]) + }, [editNodeId, updateNode, setProxmoxContainerMode, nodes, edges, deleteEdge, onConnect]) const handleAutoLayout = useCallback(() => { const laid = applyDagreLayout(nodes, edges) @@ -193,9 +218,21 @@ export default function App() { const handleEdgeConfirm = useCallback((edgeData: EdgeData) => { if (!pendingConnection) return - onConnect({ ...pendingConnection, ...edgeData }) + onConnect({ ...pendingConnection, ...edgeData } as unknown as Connection) + // When a virtual edge is drawn between LXC/VM (top) and Proxmox (bottom), sync parent_id + if (edgeData.type === 'virtual') { + const src = nodes.find((n) => n.id === pendingConnection.source) + const tgt = nodes.find((n) => n.id === pendingConnection.target) + const srcType = src?.data.type + const tgtType = tgt?.data.type + if ((srcType === 'lxc' || srcType === 'vm') && tgtType === 'proxmox') { + updateNode(pendingConnection.source, { parent_id: pendingConnection.target }) + } else if (srcType === 'proxmox' && (tgtType === 'lxc' || tgtType === 'vm')) { + updateNode(pendingConnection.target, { parent_id: pendingConnection.source }) + } + } setPendingConnection(null) - }, [pendingConnection, onConnect]) + }, [pendingConnection, onConnect, nodes, updateNode]) const handleEdgeDoubleClick = useCallback((edge: Edge) => { setEditEdgeId(edge.id) diff --git a/frontend/src/stores/canvasStore.ts b/frontend/src/stores/canvasStore.ts index 5a8288a..361a534 100644 --- a/frontend/src/stores/canvasStore.ts +++ b/frontend/src/stores/canvasStore.ts @@ -56,9 +56,15 @@ export const useCanvasStore = create((set) => ({ set((state) => { const extra = connection as Connection & Partial const edgeType = extra.type ?? 'ethernet' + // Normalize invisible stub handle IDs so React Flow can locate the handle + // and render the edge immediately (top-t / bottom-t are opacity:0 helpers). + const normalizeHandle = (h: string | null | undefined) => + h === 'top-t' ? 'top' : h === 'bottom-t' ? 'bottom' : (h ?? null) return { edges: addEdge({ ...connection, + sourceHandle: normalizeHandle(extra.sourceHandle), + targetHandle: normalizeHandle(extra.targetHandle), type: edgeType, data: { type: edgeType, label: extra.label, vlan_id: extra.vlan_id, custom_color: extra.custom_color, path_style: extra.path_style }, }, state.edges),