diff --git a/frontend/src/components/network/panels/PropertiesPanel.tsx b/frontend/src/components/network/panels/PropertiesPanel.tsx new file mode 100644 index 00000000..ee9b6b63 --- /dev/null +++ b/frontend/src/components/network/panels/PropertiesPanel.tsx @@ -0,0 +1,229 @@ +import { useCallback } from 'react' +import { Trash2 } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { DeviceProperties, DiagramEdge } from '@/types' +import type { Node, Edge } from '@xyflow/react' +import type { DeviceNodeData } from '../nodes/DeviceNode' + +interface PropertiesPanelProps { + selectedNode: Node | null + selectedEdge: Edge | null + onNodeUpdate: (nodeId: string, data: Partial) => void + onEdgeUpdate: (edgeId: string, data: Partial) => void + onDeleteNode: (nodeId: string) => void + onDeleteEdge: (edgeId: string) => void +} + +const STATUS_OPTIONS = ['unknown', 'online', 'offline', 'degraded'] as const +const CONNECTION_TYPE_OPTIONS = ['ethernet', 'fiber', 'wifi', 'vpn', 'vlan', 'wan'] as const + +function FieldLabel({ children }: { children: React.ReactNode }) { + return ( + + ) +} + +function FieldInput({ value, onChange, placeholder, mono }: { + value: string + onChange: (val: string) => void + placeholder?: string + mono?: boolean +}) { + return ( + onChange(e.target.value)} + placeholder={placeholder} + className={cn( + 'w-full rounded border border-default bg-input px-2 py-1.5 text-xs text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none', + mono && 'font-mono', + )} + /> + ) +} + +export function PropertiesPanel({ + selectedNode, + selectedEdge, + onNodeUpdate, + onEdgeUpdate, + onDeleteNode, + onDeleteEdge, +}: PropertiesPanelProps) { + const handlePropertyChange = useCallback((field: keyof DeviceProperties, value: string) => { + if (!selectedNode) return + const nodeData = selectedNode.data as unknown as DeviceNodeData + onNodeUpdate(selectedNode.id, { + properties: { ...nodeData.properties, [field]: value }, + } as Partial) + }, [selectedNode, onNodeUpdate]) + + const handleLabelChange = useCallback((value: string) => { + if (!selectedNode) return + onNodeUpdate(selectedNode.id, { label: value } as Partial) + }, [selectedNode, onNodeUpdate]) + + if (!selectedNode && !selectedEdge) { + return ( +
+

+ Select a device or connection to edit its properties +

+
+ ) + } + + if (selectedEdge) { + const edgeData = (selectedEdge.data || {}) as Record + const connectionType = (edgeData.connectionType as string) || 'ethernet' + const isCustomType = !CONNECTION_TYPE_OPTIONS.includes(connectionType as typeof CONNECTION_TYPE_OPTIONS[number]) + + return ( +
+
+

Connection

+
+
+
+ Label + onEdgeUpdate(selectedEdge.id, { label: val || null })} + placeholder="Connection label" + /> +
+
+ Connection Type + + {isCustomType && ( + onEdgeUpdate(selectedEdge.id, { connectionType: val })} + placeholder="Custom type name" + /> + )} +
+
+ Speed + onEdgeUpdate(selectedEdge.id, { speed: val || null })} + placeholder="e.g. 1 Gbps" + /> +
+
+ Notes + onEdgeUpdate(selectedEdge.id, { notes: val || null })} + placeholder="Port info, cable type..." + mono + /> +
+
+
+ +
+
+ ) + } + + const nodeData = selectedNode!.data as unknown as DeviceNodeData + const props = nodeData.properties || {} as DeviceProperties + + return ( +
+
+

Device Properties

+
+
+
+ Label + +
+
+ Hostname + handlePropertyChange('hostname', v)} placeholder="e.g. core-rtr-01" mono /> +
+
+ IP Address + handlePropertyChange('ip', v)} placeholder="e.g. 10.0.0.1" mono /> +
+
+ Subnet + handlePropertyChange('subnet', v)} placeholder="e.g. 10.0.0.0/24" mono /> +
+
+ Vendor + handlePropertyChange('vendor', v)} placeholder="e.g. Cisco" /> +
+
+ Model + handlePropertyChange('model', v)} placeholder="e.g. ISR 4331" /> +
+
+ Role + handlePropertyChange('role', v)} placeholder="e.g. Core gateway" /> +
+
+ VLAN + handlePropertyChange('vlan', v)} placeholder="e.g. 10" /> +
+
+ Status + +
+
+ Notes +