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:
@@ -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,
|
||||
|
||||
@@ -26,11 +26,14 @@ interface NodeModalProps {
|
||||
onSubmit: (data: Partial<NodeData>) => void
|
||||
initial?: Partial<NodeData>
|
||||
title?: string
|
||||
proxmoxNodes?: { id: string; label: string }[]
|
||||
}
|
||||
|
||||
const CHILD_TYPES: NodeType[] = ['vm', 'lxc']
|
||||
|
||||
// NodeModal is always mounted with a key that changes on open/edit, so useState
|
||||
// initial value is enough — no need for a reset effect.
|
||||
export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' }: NodeModalProps) {
|
||||
export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node', proxmoxNodes = [] }: NodeModalProps) {
|
||||
const [form, setForm] = useState<Partial<NodeData>>({ ...DEFAULT_DATA, ...initial })
|
||||
|
||||
const set = (key: keyof NodeData, value: unknown) =>
|
||||
@@ -129,6 +132,27 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Parent Proxmox (VM / LXC only) */}
|
||||
{CHILD_TYPES.includes(form.type as NodeType) && proxmoxNodes.length > 0 && (
|
||||
<div className="flex flex-col gap-1.5 col-span-2">
|
||||
<Label className="text-xs text-muted-foreground">Parent Proxmox</Label>
|
||||
<Select
|
||||
value={form.parent_id ?? 'none'}
|
||||
onValueChange={(v) => set('parent_id', v === 'none' ? undefined : v)}
|
||||
>
|
||||
<SelectTrigger className="bg-[#21262d] border-[#30363d] text-sm h-8">
|
||||
<SelectValue placeholder="None (standalone)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#21262d] border-[#30363d]">
|
||||
<SelectItem value="none" className="text-sm">None (standalone)</SelectItem>
|
||||
{proxmoxNodes.map((n) => (
|
||||
<SelectItem key={n.id} value={n.id} className="text-sm">{n.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
<div className="flex flex-col gap-1.5 col-span-2">
|
||||
<Label className="text-xs text-muted-foreground">Notes</Label>
|
||||
|
||||
Reference in New Issue
Block a user