feat: Proxmox nested nodes — VM/LXC rendered inside resizable group container

- ProxmoxGroupNode: resizable bordered container with orange accent, header
  bar (icon, label, IP, status dot), NodeResizer on selection
- nodeTypes: proxmox now uses ProxmoxGroupNode instead of BaseNode
- NodeData: added parent_id field
- canvasStore: addNode and loadCanvas wire parentId + extent='parent'
  for child nodes; loadCanvas sorts parents before children (RF requirement)
- NodeModal: Parent Proxmox dropdown appears when type is VM or LXC,
  lists all Proxmox nodes on the canvas
- App.tsx: passes proxmoxNodes to modals; child nodes positioned relative
  to parent (x:20, y:50); Proxmox nodes get width:300/height:200 on load
- layout.ts: Dagre skips child nodes (parentId set); uses actual group
  dimensions for Proxmox nodes in rank calculation
This commit is contained in:
Pouzor
2026-03-07 00:50:50 +01:00
parent 923f0c0c22
commit a3e6a97404
7 changed files with 151 additions and 18 deletions
+17 -6
View File
@@ -7,7 +7,8 @@ const NODE_HEIGHT = 52
/**
* Apply Dagre hierarchical (top-to-bottom) layout to nodes and edges.
* Returns new nodes with updated positions.
* Child nodes (parentId set) keep their relative position inside the parent — only
* top-level nodes are repositioned by Dagre.
*/
export function applyDagreLayout(
nodes: Node<NodeData>[],
@@ -17,22 +18,32 @@ export function applyDagreLayout(
g.setDefaultEdgeLabel(() => ({}))
g.setGraph({ rankdir: 'TB', nodesep: 60, ranksep: 80 })
for (const node of nodes) {
g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT })
const topLevel = nodes.filter((n) => !n.parentId)
for (const node of topLevel) {
const w = node.type === 'proxmox' ? (node.width ?? 300) : NODE_WIDTH
const h = node.type === 'proxmox' ? (node.height ?? 200) : NODE_HEIGHT
g.setNode(node.id, { width: w, height: h })
}
for (const edge of edges) {
g.setEdge(edge.source, edge.target)
// Only add edges between top-level nodes
const srcTop = topLevel.some((n) => n.id === edge.source)
const tgtTop = topLevel.some((n) => n.id === edge.target)
if (srcTop && tgtTop) g.setEdge(edge.source, edge.target)
}
dagre.layout(g)
return nodes.map((node) => {
if (node.parentId) return node // keep children in place
const pos = g.node(node.id)
const w = node.type === 'proxmox' ? (node.width ?? 300) : NODE_WIDTH
const h = node.type === 'proxmox' ? (node.height ?? 200) : NODE_HEIGHT
return {
...node,
position: {
x: pos.x - NODE_WIDTH / 2,
y: pos.y - NODE_HEIGHT / 2,
x: pos.x - w / 2,
y: pos.y - h / 2,
},
}
})