diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8d8a776..85a03e9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -258,10 +258,13 @@ export default function App() { ) if (oldEdge) deleteEdge(oldEdge.id) } - // Create new virtual edge: LXC top → Proxmox bottom + // Create virtual edge only when parent is NOT in container mode + // (container mode shows containment visually — no edge needed) 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) + const parentNode = nodes.find((n) => n.id === newParentId) + if (!parentNode?.data.container_mode) { + onConnect({ source: editNodeId, sourceHandle: 'top', target: newParentId, targetHandle: 'bottom', type: 'virtual' } as unknown as Connection) + } } } } diff --git a/frontend/src/stores/__tests__/canvasStore.test.ts b/frontend/src/stores/__tests__/canvasStore.test.ts index 3aa1cab..e28ed15 100644 --- a/frontend/src/stores/__tests__/canvasStore.test.ts +++ b/frontend/src/stores/__tests__/canvasStore.test.ts @@ -57,6 +57,57 @@ describe('canvasStore', () => { expect(node?.data.ip).toBe('10.0.0.1') }) + it('updateNode setting parent_id on container-mode proxmox sets parentId and relative position', () => { + const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: true }), position: { x: 100, y: 100 } } + const lxc = { ...makeNode('lxc1', { type: 'lxc' }), position: { x: 160, y: 180 } } + useCanvasStore.getState().addNode(proxmox) + useCanvasStore.getState().addNode(lxc) + useCanvasStore.getState().updateNode('lxc1', { parent_id: 'px1' }) + const node = useCanvasStore.getState().nodes.find((n) => n.id === 'lxc1') + expect(node?.parentId).toBe('px1') + expect(node?.extent).toBe('parent') + // Position should be relative to parent (160-100=60, 180-100=80) + expect(node?.position.x).toBe(60) + expect(node?.position.y).toBe(80) + }) + + it('updateNode setting parent_id on non-container proxmox does NOT set React Flow parentId', () => { + const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: false }), position: { x: 100, y: 100 } } + const lxc = { ...makeNode('lxc1', { type: 'lxc' }), position: { x: 160, y: 180 } } + useCanvasStore.getState().addNode(proxmox) + useCanvasStore.getState().addNode(lxc) + useCanvasStore.getState().updateNode('lxc1', { parent_id: 'px1' }) + const node = useCanvasStore.getState().nodes.find((n) => n.id === 'lxc1') + expect(node?.parentId).toBeUndefined() + expect(node?.extent).toBeUndefined() + }) + + it('updateNode clearing parent_id converts position to absolute and clears parentId', () => { + const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: true }), position: { x: 100, y: 100 } } + const lxc = { ...makeNode('lxc1', { type: 'lxc', parent_id: 'px1' }), position: { x: 30, y: 40 }, parentId: 'px1', extent: 'parent' as const } + useCanvasStore.getState().addNode(proxmox) + useCanvasStore.getState().addNode(lxc) + useCanvasStore.getState().updateNode('lxc1', { parent_id: undefined }) + const node = useCanvasStore.getState().nodes.find((n) => n.id === 'lxc1') + expect(node?.parentId).toBeUndefined() + expect(node?.extent).toBeUndefined() + // Position should be absolute (100+30=130, 100+40=140) + expect(node?.position.x).toBe(130) + expect(node?.position.y).toBe(140) + }) + + it('updateNode with parent_id puts parents before children in array', () => { + const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: true }), position: { x: 0, y: 0 } } + const lxc = { ...makeNode('lxc1', { type: 'lxc' }), position: { x: 10, y: 10 } } + useCanvasStore.getState().addNode(proxmox) + useCanvasStore.getState().addNode(lxc) + useCanvasStore.getState().updateNode('lxc1', { parent_id: 'px1' }) + const { nodes } = useCanvasStore.getState() + const pxIdx = nodes.findIndex((n) => n.id === 'px1') + const lxcIdx = nodes.findIndex((n) => n.id === 'lxc1') + expect(pxIdx).toBeLessThan(lxcIdx) + }) + it('deleteNode removes node and its connected edges', () => { const store = useCanvasStore.getState() store.addNode(makeNode('n1')) diff --git a/frontend/src/stores/canvasStore.ts b/frontend/src/stores/canvasStore.ts index 6525495..7416423 100644 --- a/frontend/src/stores/canvasStore.ts +++ b/frontend/src/stores/canvasStore.ts @@ -187,12 +187,47 @@ export const useCanvasStore = create((set) => ({ }), updateNode: (id, data) => - set((state) => ({ - nodes: state.nodes.map((n) => - n.id === id ? { ...n, data: { ...n.data, ...data } } : n - ), - hasUnsavedChanges: true, - })), + set((state) => { + let nodes = state.nodes.map((n) => { + if (n.id !== id) return n + const updated: Node = { ...n, data: { ...n.data, ...data } } + if ('parent_id' in data) { + const newParentId = data.parent_id ?? undefined + if (!newParentId && n.parentId) { + // Detaching from a container: convert position back to absolute canvas coords + const parent = state.nodes.find((p) => p.id === n.parentId) + if (parent) { + updated.position = { + x: parent.position.x + n.position.x, + y: parent.position.y + n.position.y, + } + } + updated.parentId = undefined + updated.extent = undefined + } else if (newParentId && newParentId !== n.parentId) { + const parent = state.nodes.find((p) => p.id === newParentId) + if (parent?.data.container_mode) { + // Attaching to a container-mode Proxmox: nest visually + updated.parentId = newParentId + updated.extent = 'parent' as const + // Convert absolute position to parent-relative (keep node visible inside) + updated.position = { + x: Math.max(10, n.position.x - parent.position.x), + y: Math.max(10, n.position.y - parent.position.y), + } + } + } + } + return updated + }) + // React Flow requires parent nodes to precede their children in the array + if ('parent_id' in data) { + const parents = nodes.filter((n) => !n.parentId) + const children = nodes.filter((n) => !!n.parentId) + nodes = [...parents, ...children] + } + return { nodes, hasUnsavedChanges: true } + }), deleteNode: (id) => set((state) => {