diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d18b3b1..069ecb6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -245,9 +245,16 @@ export default function App() { const id = generateUUID() const isContainerNode = data.container_mode === true const parentNode = data.parent_id ? nodes.find((n) => n.id === data.parent_id) : null - // Children position is relative to parent; place near top-left with padding - const position = parentNode - ? { x: 20, y: 50 } + // Only nest when the parent is an actual container. For a non-container + // parent the LXC/VM stays a free node (linked by a virtual edge) — setting + // extent:'parent' on a non-container would trap it inside the parent's tiny + // bounding box with no way to drag it out (issue #205 follow-up). + const nestInParent = !!parentNode?.data.container_mode + // Seed an ABSOLUTE position near the container's top-left; addNode converts + // it to container-relative. addNode is the single authority for parentId / + // extent, so we don't set them here. + const position = nestInParent && parentNode + ? { x: parentNode.position.x + 20, y: parentNode.position.y + 50 } : { x: 300, y: 300 } const newNode: Node = { @@ -255,7 +262,6 @@ export default function App() { type: data.type ?? 'generic', position, data: { status: 'unknown', services: [], ...data } as NodeData, - ...(data.parent_id ? { parentId: data.parent_id, extent: 'parent' as const } : {}), ...(isContainerNode ? { width: 300, height: 200 } : {}), } addNode(newNode) diff --git a/frontend/src/stores/__tests__/canvasStore.test.ts b/frontend/src/stores/__tests__/canvasStore.test.ts index 3f39c0a..9c69b99 100644 --- a/frontend/src/stores/__tests__/canvasStore.test.ts +++ b/frontend/src/stores/__tests__/canvasStore.test.ts @@ -150,6 +150,23 @@ describe('canvasStore', () => { expect(nested?.extent).toBe('parent') }) + it('addNode strips parentId/extent when the parent is not a container', () => { + // Regression: a stray extent:'parent' on a non-container parent traps the + // node in the parent's tiny box with no way to drag it out (issue #205). + const parent = { ...makeNode('p1', { container_mode: false }), position: { x: 100, y: 100 } } + useCanvasStore.getState().addNode(parent) + const trapped: Node = { + ...makeNode('c1', { parent_id: 'p1' }), + position: { x: 150, y: 180 }, + parentId: 'p1', + extent: 'parent', + } + useCanvasStore.getState().addNode(trapped) + const child = useCanvasStore.getState().nodes.find((n) => n.id === 'c1') + expect(child?.parentId).toBeUndefined() + expect(child?.extent).toBeUndefined() + }) + it('docker_container nests under docker_host with container_mode on', () => { const host = { ...makeNode('dh1', { type: 'docker_host', container_mode: true }), position: { x: 100, y: 100 } } const container = { ...makeNode('dc1', { type: 'docker_container' }), position: { x: 160, y: 180 } } diff --git a/frontend/src/stores/canvasStore.ts b/frontend/src/stores/canvasStore.ts index baad428..b1ba24e 100644 --- a/frontend/src/stores/canvasStore.ts +++ b/frontend/src/stores/canvasStore.ts @@ -289,7 +289,9 @@ export const useCanvasStore = create((set) => ({ y: Math.max(10, node.position.y - parent.position.y), }, } - : node + // Not nesting: strip any parentId/extent a caller may have set so a + // non-container parent can't trap the node in its bounding box. + : { ...node, parentId: undefined, extent: undefined } // Parents must come before children in the array (React Flow requirement) const withoutNew = state.nodes.filter((n) => n.id !== node.id) if (enriched.parentId) {