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
+16 -3
View File
@@ -57,11 +57,13 @@ export default function App() {
.then((res) => {
const { nodes: apiNodes, edges: apiEdges } = res.data
if (apiNodes.length > 0) {
const rfNodes = apiNodes.map((n: NodeData & { id: string; pos_x: number; pos_y: number }) => ({
const rfNodes = apiNodes.map((n: NodeData & { id: string; pos_x: number; pos_y: number; parent_id?: string }) => ({
id: n.id,
type: n.type,
position: { x: n.pos_x, y: n.pos_y },
data: n,
...(n.parent_id ? { parentId: n.parent_id, extent: 'parent' as const } : {}),
...(n.type === 'proxmox' ? { width: 300, height: 200 } : {}),
}))
const rfEdges = apiEdges.map((e: EdgeData & { id: string; source: string; target: string }) => ({
id: e.id,
@@ -92,15 +94,24 @@ export default function App() {
const handleAddNode = useCallback((data: Partial<NodeData>) => {
const id = crypto.randomUUID()
const isProxmox = data.type === 'proxmox'
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 }
: { x: 300, y: 300 }
const newNode: Node<NodeData> = {
id,
type: data.type ?? 'generic',
position: { x: 300, y: 300 },
position,
data: { status: 'unknown', services: [], ...data } as NodeData,
...(data.parent_id ? { parentId: data.parent_id, extent: 'parent' as const } : {}),
...(isProxmox ? { width: 300, height: 200 } : {}),
}
addNode(newNode)
toast.success(`Added "${data.label}"`)
}, [addNode])
}, [addNode, nodes])
const handleEditNode = useCallback((id: string) => {
setEditNodeId(id)
@@ -172,6 +183,7 @@ export default function App() {
onClose={() => setAddNodeOpen(false)}
onSubmit={handleAddNode}
title="Add Node"
proxmoxNodes={nodes.filter((n) => n.type === 'proxmox').map((n) => ({ id: n.id, label: n.data.label }))}
/>
{/* key forces re-mount when editing a different node, resetting form state */}
@@ -182,6 +194,7 @@ export default function App() {
onSubmit={handleUpdateNode}
initial={editNode?.data}
title="Edit Node"
proxmoxNodes={nodes.filter((n) => n.type === 'proxmox').map((n) => ({ id: n.id, label: n.data.label }))}
/>
<EdgeModal