From 354b44844cccc37a5678ee66c4860d34a721fb14 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sat, 4 Apr 2026 07:50:10 +0000 Subject: [PATCH] feat: add device registry, DeviceNode, ConnectionEdge for React Flow Creates the React Flow building blocks for the network diagram editor: device type registry with icon/color mappings, DeviceNode component with status indicators and connection handles, ConnectionEdge with per-type styling, and nodeTypes/edgeTypes registration maps. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../network/edges/ConnectionEdge.tsx | 54 ++++++++++++++ .../src/components/network/edges/edgeTypes.ts | 5 ++ .../components/network/nodes/DeviceNode.tsx | 51 +++++++++++++ .../network/nodes/deviceRegistry.ts | 73 +++++++++++++++++++ .../src/components/network/nodes/nodeTypes.ts | 5 ++ 5 files changed, 188 insertions(+) create mode 100644 frontend/src/components/network/edges/ConnectionEdge.tsx create mode 100644 frontend/src/components/network/edges/edgeTypes.ts create mode 100644 frontend/src/components/network/nodes/DeviceNode.tsx create mode 100644 frontend/src/components/network/nodes/deviceRegistry.ts create mode 100644 frontend/src/components/network/nodes/nodeTypes.ts diff --git a/frontend/src/components/network/edges/ConnectionEdge.tsx b/frontend/src/components/network/edges/ConnectionEdge.tsx new file mode 100644 index 00000000..0e38ba72 --- /dev/null +++ b/frontend/src/components/network/edges/ConnectionEdge.tsx @@ -0,0 +1,54 @@ +import { memo } from 'react' +import { SmoothStepEdge, EdgeLabelRenderer, type EdgeProps } from '@xyflow/react' + +interface ConnectionEdgeData { + connectionType?: string + speed?: string | null + notes?: string | null + [key: string]: unknown +} + +const CONNECTION_STYLES: Record = { + ethernet: { stroke: '#60a5fa', strokeWidth: 2 }, + fiber: { stroke: '#34d399', strokeWidth: 3 }, + wifi: { stroke: '#a78bfa', strokeDasharray: '3,3', strokeWidth: 2 }, + vpn: { stroke: '#eab308', strokeDasharray: '8,4', strokeWidth: 2 }, + vlan: { stroke: '#848b9b', strokeWidth: 2 }, + wan: { stroke: '#f87171', strokeDasharray: '12,4', strokeWidth: 2 }, +} + +const DEFAULT_STYLE = { stroke: '#848b9b', strokeWidth: 2 } + +function ConnectionEdgeComponent(props: EdgeProps) { + const edgeData = props.data as ConnectionEdgeData | undefined + const connectionType = edgeData?.connectionType || 'ethernet' + const style = CONNECTION_STYLES[connectionType] || DEFAULT_STYLE + + return ( + <> + + {props.label && ( + +
+ {props.label} +
+
+ )} + + ) +} + +export const ConnectionEdge = memo(ConnectionEdgeComponent) diff --git a/frontend/src/components/network/edges/edgeTypes.ts b/frontend/src/components/network/edges/edgeTypes.ts new file mode 100644 index 00000000..0c23c2f8 --- /dev/null +++ b/frontend/src/components/network/edges/edgeTypes.ts @@ -0,0 +1,5 @@ +import { ConnectionEdge } from './ConnectionEdge' + +export const edgeTypes = { + connection: ConnectionEdge, +} diff --git a/frontend/src/components/network/nodes/DeviceNode.tsx b/frontend/src/components/network/nodes/DeviceNode.tsx new file mode 100644 index 00000000..b0de9f61 --- /dev/null +++ b/frontend/src/components/network/nodes/DeviceNode.tsx @@ -0,0 +1,51 @@ +import { memo } from 'react' +import { Handle, Position, type NodeProps } from '@xyflow/react' +import { cn } from '@/lib/utils' +import { getDeviceRenderConfig } from './deviceRegistry' +import type { DeviceProperties } from '@/types' + +export interface DeviceNodeData { + label: string + deviceType: string + category?: string + properties: DeviceProperties + [key: string]: unknown +} + +const STATUS_COLORS: Record = { + online: 'bg-emerald-400', + offline: 'bg-red-400', + degraded: 'bg-yellow-400', + unknown: 'bg-gray-500', +} + +function DeviceNodeComponent({ data, selected }: NodeProps) { + const nodeData = data as unknown as DeviceNodeData + const { icon: Icon, color } = getDeviceRenderConfig(nodeData.deviceType, nodeData.category) + const status = nodeData.properties?.status || 'unknown' + const ip = nodeData.properties?.ip + + return ( +
+
+ + {nodeData.label} + {ip && ( + {ip} + )} + + + + +
+ ) +} + +export const DeviceNode = memo(DeviceNodeComponent) diff --git a/frontend/src/components/network/nodes/deviceRegistry.ts b/frontend/src/components/network/nodes/deviceRegistry.ts new file mode 100644 index 00000000..a5d7b4a4 --- /dev/null +++ b/frontend/src/components/network/nodes/deviceRegistry.ts @@ -0,0 +1,73 @@ +import type { LucideIcon } from 'lucide-react' +import { + Network, Layers, Shield, Wifi, Server, Monitor, Box, Cloud, + Printer, Smartphone, HardDrive, Scale, Database, CloudCog, + Cpu, Tablet, Laptop, BatteryCharging, LayoutGrid, RectangleVertical, + Cable, Camera, KeyRound, +} from 'lucide-react' + +export interface DeviceRenderConfig { + icon: LucideIcon + color: string +} + +const SYSTEM_DEVICE_ICONS: Record = { + 'router': { icon: Network, color: 'var(--color-accent)' }, + 'switch': { icon: Layers, color: 'var(--color-text-muted-foreground)' }, + 'firewall': { icon: Shield, color: 'var(--color-accent)' }, + 'access-point': { icon: Wifi, color: 'var(--color-text-muted-foreground)' }, + 'load-balancer': { icon: Scale, color: 'var(--color-text-muted-foreground)' }, + 'server': { icon: Server, color: 'var(--color-text-muted-foreground)' }, + 'workstation': { icon: Monitor, color: 'var(--color-text-muted-foreground)' }, + 'vm': { icon: Box, color: 'var(--color-text-muted-foreground)' }, + 'container': { icon: Cpu, color: 'var(--color-text-muted-foreground)' }, + 'nas': { icon: Database, color: 'var(--color-text-muted-foreground)' }, + 'san': { icon: HardDrive, color: 'var(--color-text-muted-foreground)' }, + 'cloud-storage': { icon: CloudCog, color: 'var(--color-text-muted-foreground)' }, + 'cloud': { icon: Cloud, color: 'var(--color-text-muted-foreground)' }, + 'aws': { icon: Cloud, color: 'var(--color-text-muted-foreground)' }, + 'azure': { icon: Cloud, color: 'var(--color-text-muted-foreground)' }, + 'gcp': { icon: Cloud, color: 'var(--color-text-muted-foreground)' }, + 'printer': { icon: Printer, color: 'var(--color-text-muted-foreground)' }, + 'phone': { icon: Smartphone, color: 'var(--color-text-muted-foreground)' }, + 'iot': { icon: HardDrive, color: 'var(--color-text-muted-foreground)' }, + 'camera': { icon: Camera, color: 'var(--color-text-muted-foreground)' }, + 'tablet': { icon: Tablet, color: 'var(--color-text-muted-foreground)' }, + 'laptop': { icon: Laptop, color: 'var(--color-text-muted-foreground)' }, + 'ups': { icon: BatteryCharging, color: 'var(--color-text-muted-foreground)' }, + 'pdu': { icon: LayoutGrid, color: 'var(--color-text-muted-foreground)' }, + 'rack': { icon: RectangleVertical, color: 'var(--color-text-muted-foreground)' }, + 'patch-panel': { icon: Cable, color: 'var(--color-text-muted-foreground)' }, + 'nvr': { icon: Camera, color: 'var(--color-text-muted-foreground)' }, + 'badge-reader': { icon: KeyRound, color: 'var(--color-text-muted-foreground)' }, +} + +const CATEGORY_DEFAULTS: Record = { + 'network': { icon: Network, color: 'var(--color-text-muted-foreground)' }, + 'compute': { icon: Server, color: 'var(--color-text-muted-foreground)' }, + 'storage': { icon: Database, color: 'var(--color-text-muted-foreground)' }, + 'cloud': { icon: Cloud, color: 'var(--color-text-muted-foreground)' }, + 'endpoint': { icon: Monitor, color: 'var(--color-text-muted-foreground)' }, + 'infrastructure': { icon: LayoutGrid, color: 'var(--color-text-muted-foreground)' }, + 'security': { icon: Shield, color: 'var(--color-text-muted-foreground)' }, +} + +const FALLBACK: DeviceRenderConfig = { icon: Box, color: 'var(--color-text-muted-foreground)' } + +export function getDeviceRenderConfig(slug: string, category?: string): DeviceRenderConfig { + if (SYSTEM_DEVICE_ICONS[slug]) return SYSTEM_DEVICE_ICONS[slug] + if (category && CATEGORY_DEFAULTS[category]) return CATEGORY_DEFAULTS[category] + return FALLBACK +} + +export const CATEGORY_LABELS: Record = { + 'network': 'Network', + 'compute': 'Compute', + 'storage': 'Storage', + 'cloud': 'Cloud', + 'endpoint': 'Endpoints', + 'infrastructure': 'Infrastructure', + 'security': 'Security', +} + +export const CATEGORY_ORDER = ['network', 'compute', 'storage', 'cloud', 'endpoint', 'infrastructure', 'security'] diff --git a/frontend/src/components/network/nodes/nodeTypes.ts b/frontend/src/components/network/nodes/nodeTypes.ts new file mode 100644 index 00000000..d698081c --- /dev/null +++ b/frontend/src/components/network/nodes/nodeTypes.ts @@ -0,0 +1,5 @@ +import { DeviceNode } from './DeviceNode' + +export const nodeTypes = { + device: DeviceNode, +}