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
@@ -0,0 +1,72 @@
import { Handle, Position, NodeResizer, type NodeProps, type Node } from '@xyflow/react'
import { Layers } from 'lucide-react'
import type { NodeData, NodeStatus } from '@/types'
const STATUS_COLORS: Record<NodeStatus, string> = {
online: '#39d353',
offline: '#f85149',
pending: '#e3b341',
unknown: '#8b949e',
}
const GLOW = '#ff6e00'
export function ProxmoxGroupNode({ data, selected }: NodeProps<Node<NodeData>>) {
const statusColor = STATUS_COLORS[data.status]
const isOnline = data.status === 'online'
return (
<>
<NodeResizer
minWidth={220}
minHeight={160}
isVisible={selected}
lineStyle={{ borderColor: GLOW, opacity: 0.6 }}
handleStyle={{ borderColor: GLOW, backgroundColor: '#21262d' }}
/>
{/* Group border */}
<div
className="w-full h-full rounded-xl border-2 flex flex-col overflow-hidden"
style={{
borderColor: selected ? GLOW : isOnline ? `${GLOW}66` : '#30363d',
background: isOnline ? `${GLOW}08` : '#0d111766',
boxShadow: isOnline
? `0 0 20px ${GLOW}1a, inset 0 0 40px ${GLOW}08`
: selected
? `0 0 12px ${GLOW}33`
: 'none',
}}
>
{/* Header bar */}
<div
className="flex items-center gap-2 px-2.5 py-1.5 shrink-0"
style={{ background: isOnline ? `${GLOW}18` : '#161b2288', borderBottom: `1px solid ${isOnline ? `${GLOW}33` : '#30363d'}` }}
>
<div
className="flex items-center justify-center w-5 h-5 rounded-md shrink-0"
style={{ color: isOnline ? GLOW : '#8b949e', background: '#161b22' }}
>
<Layers size={12} />
</div>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-[11px] font-semibold leading-tight truncate" style={{ color: isOnline ? GLOW : '#c9d1d9' }}>
{data.label}
</span>
{data.ip && (
<span className="font-mono text-[9px] text-[#8b949e] truncate">{data.ip}</span>
)}
</div>
{/* Status dot */}
<div className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: statusColor }} title={data.status} />
</div>
{/* Inner area — React Flow places children here */}
<div className="flex-1 relative" />
</div>
<Handle type="target" position={Position.Top} className="!bg-[#30363d] !border-[#8b949e]" />
<Handle type="source" position={Position.Bottom} className="!bg-[#30363d] !border-[#8b949e]" />
</>
)
}
@@ -1,11 +1,12 @@
import { IspNode, RouterNode, SwitchNode, ServerNode, ProxmoxNode, VmNode, LxcNode, NasNode, IotNode, ApNode, GenericNode } from './index'
import { IspNode, RouterNode, SwitchNode, ServerNode, VmNode, LxcNode, NasNode, IotNode, ApNode, GenericNode } from './index'
import { ProxmoxGroupNode } from './ProxmoxGroupNode'
export const nodeTypes = {
isp: IspNode,
router: RouterNode,
switch: SwitchNode,
server: ServerNode,
proxmox: ProxmoxNode,
proxmox: ProxmoxGroupNode,
vm: VmNode,
lxc: LxcNode,
nas: NasNode,