From b5b1056ae6e52ea0626a7a326ee53508029bbf60 Mon Sep 17 00:00:00 2001 From: Cyril Grosse III Date: Thu, 28 May 2026 15:55:46 -0400 Subject: [PATCH] Add laptop and mobile node types Personal computing devices (laptops, phones, tablets) currently collapse into the generic icon because the type vocabulary has no entries for them. This adds two new NodeTypes with Lucide icons: - laptop -> Laptop icon (reuses the computer accent color per theme) - mobile -> Smartphone icon (new pink accent #ec4899 across themes) Touches: - types/index.ts NodeType union + NODE_TYPE_LABELS - utils/nodeIcons.ts Lucide import + ICON_REGISTRY + NODE_TYPE_DEFAULT_ICONS - utils/themes.ts nodeAccents in all 6 themes - canvas/nodes/index.tsx LaptopNode + MobileNode wrappers - canvas/nodes/nodeTypes.ts register in react-flow nodeTypes - modals/NodeModal.tsx new "Personal" type group - modals/CustomStyleModal.tsx expose new types in style editor - types/__tests__/types.test.ts enumerate new types - utils/__tests__/themes.test.ts enumerate new types Backwards-compatible: existing nodes typed as 'generic', 'server', etc. keep rendering exactly as before. No data migration required. --- frontend/src/components/canvas/nodes/index.tsx | 4 +++- frontend/src/components/canvas/nodes/nodeTypes.ts | 4 +++- frontend/src/components/modals/CustomStyleModal.tsx | 6 +++--- frontend/src/components/modals/NodeModal.tsx | 3 ++- frontend/src/types/__tests__/types.test.ts | 2 +- frontend/src/types/index.ts | 4 ++++ frontend/src/utils/__tests__/themes.test.ts | 2 +- frontend/src/utils/nodeIcons.ts | 5 ++++- frontend/src/utils/themes.ts | 12 ++++++++++++ 9 files changed, 33 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/canvas/nodes/index.tsx b/frontend/src/components/canvas/nodes/index.tsx index 6bf6eff..1cb114b 100644 --- a/frontend/src/components/canvas/nodes/index.tsx +++ b/frontend/src/components/canvas/nodes/index.tsx @@ -1,7 +1,7 @@ import { type NodeProps, type Node } from '@xyflow/react' import { Globe, Router, Network, Server, Layers, Box, Container, - HardDrive, Cpu, Wifi, Circle, Cctv, Printer, Monitor, PlugZap, Anchor, Package, Flame, Radio, Antenna, + HardDrive, Cpu, Wifi, Circle, Cctv, Printer, Monitor, Laptop, Smartphone, PlugZap, Anchor, Package, Flame, Radio, Antenna, } from 'lucide-react' import { BaseNode } from './BaseNode' import type { NodeData } from '@/types' @@ -22,6 +22,8 @@ export const ApNode = (props: N) => export const CameraNode = (props: N) => export const PrinterNode = (props: N) => export const ComputerNode = (props: N) => +export const LaptopNode = (props: N) => +export const MobileNode = (props: N) => export const CplNode = (props: N) => export const DockerHostNode = (props: N) => export const DockerContainerNode = (props: N) => diff --git a/frontend/src/components/canvas/nodes/nodeTypes.ts b/frontend/src/components/canvas/nodes/nodeTypes.ts index 34d25ec..38e427f 100644 --- a/frontend/src/components/canvas/nodes/nodeTypes.ts +++ b/frontend/src/components/canvas/nodes/nodeTypes.ts @@ -1,4 +1,4 @@ -import { IspNode, RouterNode, FirewallNode, SwitchNode, ServerNode, VmNode, LxcNode, NasNode, IotNode, ApNode, CameraNode, PrinterNode, ComputerNode, CplNode, DockerHostNode, DockerContainerNode, GenericNode, ZigbeeCoordinatorNode, ZigbeeRouterNode, ZigbeeEndDeviceNode } from './index' +import { IspNode, RouterNode, FirewallNode, SwitchNode, ServerNode, VmNode, LxcNode, NasNode, IotNode, ApNode, CameraNode, PrinterNode, ComputerNode, LaptopNode, MobileNode, CplNode, DockerHostNode, DockerContainerNode, GenericNode, ZigbeeCoordinatorNode, ZigbeeRouterNode, ZigbeeEndDeviceNode } from './index' import { ProxmoxGroupNode } from './ProxmoxGroupNode' import { GroupRectNode } from './GroupRectNode' import { GroupNode } from './GroupNode' @@ -19,6 +19,8 @@ export const nodeTypes = { camera: CameraNode, printer: PrinterNode, computer: ComputerNode, + laptop: LaptopNode, + mobile: MobileNode, cpl: CplNode, docker_host: DockerHostNode, docker_container: DockerContainerNode, diff --git a/frontend/src/components/modals/CustomStyleModal.tsx b/frontend/src/components/modals/CustomStyleModal.tsx index 20aaac1..09391a0 100644 --- a/frontend/src/components/modals/CustomStyleModal.tsx +++ b/frontend/src/components/modals/CustomStyleModal.tsx @@ -2,7 +2,7 @@ import { useState, useCallback } from 'react' import { toast } from 'sonner' import { Globe, Router, Network, Server, Layers, Box, Container, HardDrive, - Cpu, Wifi, Camera, Printer, Monitor, PlugZap, Anchor, Package, Circle, Flame, + Cpu, Wifi, Camera, Printer, Monitor, Laptop, Smartphone, PlugZap, Anchor, Package, Circle, Flame, Radio, Zap, Lightbulb, type LucideIcon, } from 'lucide-react' @@ -21,7 +21,7 @@ import { NODE_TYPE_LABELS, EDGE_TYPE_LABELS } from '@/types' const EDITABLE_NODE_TYPES: NodeType[] = [ 'isp', 'router', 'firewall', 'switch', 'server', 'proxmox', 'vm', 'lxc', 'nas', - 'iot', 'ap', 'camera', 'printer', 'computer', 'cpl', 'docker_host', + 'iot', 'ap', 'camera', 'printer', 'computer', 'laptop', 'mobile', 'cpl', 'docker_host', 'docker_container', 'zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice', 'generic', ] @@ -31,7 +31,7 @@ const EDITABLE_EDGE_TYPES: EdgeType[] = ['ethernet', 'wifi', 'iot', 'vlan', 'vir const NODE_ICONS: Record = { isp: Globe, router: Router, firewall: Flame, switch: Network, server: Server, proxmox: Layers, vm: Box, lxc: Container, nas: HardDrive, iot: Cpu, ap: Wifi, - camera: Camera, printer: Printer, computer: Monitor, cpl: PlugZap, + camera: Camera, printer: Printer, computer: Monitor, laptop: Laptop, mobile: Smartphone, cpl: PlugZap, docker_host: Anchor, docker_container: Package, zigbee_coordinator: Radio, zigbee_router: Zap, zigbee_enddevice: Lightbulb, generic: Circle, diff --git a/frontend/src/components/modals/NodeModal.tsx b/frontend/src/components/modals/NodeModal.tsx index d5b851f..274954b 100644 --- a/frontend/src/components/modals/NodeModal.tsx +++ b/frontend/src/components/modals/NodeModal.tsx @@ -18,7 +18,8 @@ const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [ { label: 'Virtualization', types: ['proxmox', 'vm', 'lxc', 'docker_host', 'docker_container'] }, { label: 'IoT', types: ['iot', 'camera', 'cpl'] }, { label: 'Zigbee', types: ['zigbee_coordinator', 'zigbee_router', 'zigbee_enddevice'] }, - { label: 'Generic', types: ['computer', 'generic', 'groupRect'] }, + { label: 'Personal', types: ['computer', 'laptop', 'mobile'] }, + { label: 'Generic', types: ['generic', 'groupRect'] }, ] const CHECK_METHODS: CheckMethod[] = ['none', 'ping', 'http', 'https', 'tcp', 'ssh', 'prometheus', 'health'] diff --git a/frontend/src/types/__tests__/types.test.ts b/frontend/src/types/__tests__/types.test.ts index 57e6efd..0ccf129 100644 --- a/frontend/src/types/__tests__/types.test.ts +++ b/frontend/src/types/__tests__/types.test.ts @@ -4,7 +4,7 @@ import type { CheckMethod } from '@/types' describe('NODE_TYPE_LABELS', () => { it('has an entry for every node type', () => { - const expectedTypes = ['isp', 'router', 'firewall', 'switch', 'server', 'proxmox', 'vm', 'lxc', 'nas', 'iot', 'ap', 'camera', 'generic'] + const expectedTypes = ['isp', 'router', 'firewall', 'switch', 'server', 'proxmox', 'vm', 'lxc', 'nas', 'iot', 'ap', 'camera', 'laptop', 'mobile', 'generic'] expectedTypes.forEach((t) => { expect(NODE_TYPE_LABELS).toHaveProperty(t) expect(typeof NODE_TYPE_LABELS[t as keyof typeof NODE_TYPE_LABELS]).toBe('string') diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 8e79fa6..e159821 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -13,6 +13,8 @@ export type NodeType = | 'camera' | 'printer' | 'computer' + | 'laptop' + | 'mobile' | 'cpl' | 'docker_host' | 'docker_container' @@ -137,6 +139,8 @@ export const NODE_TYPE_LABELS: Record = { camera: 'Camera', printer: 'Printer', computer: 'Computer', + laptop: 'Laptop', + mobile: 'Phone / Mobile', cpl: 'CPL / Powerline', docker_host: 'Docker Host', docker_container: 'Docker Container', diff --git a/frontend/src/utils/__tests__/themes.test.ts b/frontend/src/utils/__tests__/themes.test.ts index 1a4b103..74e7937 100644 --- a/frontend/src/utils/__tests__/themes.test.ts +++ b/frontend/src/utils/__tests__/themes.test.ts @@ -4,7 +4,7 @@ import type { NodeType, EdgeType, NodeStatus } from '@/types' const NODE_TYPES: NodeType[] = [ 'isp', 'router', 'firewall', 'switch', 'server', 'proxmox', 'vm', 'lxc', - 'nas', 'iot', 'ap', 'camera', 'printer', 'computer', 'cpl', 'docker_host', 'docker_container', 'generic', 'groupRect', + 'nas', 'iot', 'ap', 'camera', 'printer', 'computer', 'laptop', 'mobile', 'cpl', 'docker_host', 'docker_container', 'generic', 'groupRect', ] const EDGE_TYPES: EdgeType[] = ['ethernet', 'wifi', 'iot', 'vlan', 'virtual', 'cluster'] const STATUS_TYPES: NodeStatus[] = ['online', 'offline', 'pending', 'unknown'] diff --git a/frontend/src/utils/nodeIcons.ts b/frontend/src/utils/nodeIcons.ts index c781d0a..6a5d2a3 100644 --- a/frontend/src/utils/nodeIcons.ts +++ b/frontend/src/utils/nodeIcons.ts @@ -24,7 +24,7 @@ import { // Communications Mail, MessageSquare, Phone, // Misc devices - Printer, Smartphone, Search, Filter, BookOpen, PlugZap, Type, + Printer, Smartphone, Laptop, Search, Filter, BookOpen, PlugZap, Type, } from 'lucide-react' import type { LucideIcon } from 'lucide-react' @@ -50,6 +50,7 @@ export const ICON_REGISTRY: IconEntry[] = [ { key: 'wifi', label: 'Access Point', category: 'Infrastructure', icon: Wifi }, { key: 'circle', label: 'Generic', category: 'Infrastructure', icon: Circle }, { key: 'monitor', label: 'Workstation', category: 'Infrastructure', icon: Monitor }, + { key: 'laptop', label: 'Laptop', category: 'Infrastructure', icon: Laptop }, { key: 'smartphone', label: 'Phone / Mobile', category: 'Infrastructure', icon: Smartphone }, { key: 'printer', label: 'Printer', category: 'Infrastructure', icon: Printer }, { key: 'plugzap', label: 'CPL / Powerline', category: 'Infrastructure', icon: PlugZap }, @@ -167,6 +168,8 @@ export const NODE_TYPE_DEFAULT_ICONS: Record = { camera: Cctv, printer: Printer, computer: Monitor, + laptop: Laptop, + mobile: Smartphone, cpl: PlugZap, docker_host: Anchor, docker_container: Package, diff --git a/frontend/src/utils/themes.ts b/frontend/src/utils/themes.ts index 82c03e4..3762731 100644 --- a/frontend/src/utils/themes.ts +++ b/frontend/src/utils/themes.ts @@ -56,6 +56,8 @@ export const THEMES: Record = { camera: { border: '#8b949e', icon: '#8b949e' }, printer: { border: '#8b949e', icon: '#8b949e' }, computer: { border: '#a855f7', icon: '#a855f7' }, + laptop: { border: '#a855f7', icon: '#a855f7' }, + mobile: { border: '#ec4899', icon: '#ec4899' }, cpl: { border: '#e3b341', icon: '#e3b341' }, docker_host: { border: '#2496ED', icon: '#2496ED' }, docker_container: { border: '#0ea5e9', icon: '#0ea5e9' }, @@ -117,6 +119,8 @@ export const THEMES: Record = { camera: { border: '#94a3b8', icon: '#94a3b8' }, printer: { border: '#94a3b8', icon: '#94a3b8' }, computer: { border: '#c084fc', icon: '#c084fc' }, + laptop: { border: '#c084fc', icon: '#c084fc' }, + mobile: { border: '#ec4899', icon: '#ec4899' }, cpl: { border: '#fbbf24', icon: '#fbbf24' }, docker_host: { border: '#2496ED', icon: '#2496ED' }, docker_container: { border: '#38bdf8', icon: '#38bdf8' }, @@ -178,6 +182,8 @@ export const THEMES: Record = { camera: { border: '#6b7280', icon: '#6b7280' }, printer: { border: '#6b7280', icon: '#6b7280' }, computer: { border: '#7c3aed', icon: '#7c3aed' }, + laptop: { border: '#7c3aed', icon: '#7c3aed' }, + mobile: { border: '#ec4899', icon: '#ec4899' }, cpl: { border: '#b45309', icon: '#b45309' }, docker_host: { border: '#2496ED', icon: '#2496ED' }, docker_container: { border: '#0369a1', icon: '#0369a1' }, @@ -239,6 +245,8 @@ export const THEMES: Record = { camera: { border: '#8888ff', icon: '#8888ff' }, printer: { border: '#8888ff', icon: '#8888ff' }, computer: { border: '#ff00ff', icon: '#ff00ff' }, + laptop: { border: '#ff00ff', icon: '#ff00ff' }, + mobile: { border: '#ec4899', icon: '#ec4899' }, cpl: { border: '#ffff00', icon: '#ffff00' }, docker_host: { border: '#00aaff', icon: '#00aaff' }, docker_container: { border: '#00ddff', icon: '#00ddff' }, @@ -300,6 +308,8 @@ export const THEMES: Record = { camera: { border: '#005500', icon: '#005500' }, printer: { border: '#005500', icon: '#005500' }, computer: { border: '#008822', icon: '#008822' }, + laptop: { border: '#008822', icon: '#008822' }, + mobile: { border: '#ec4899', icon: '#ec4899' }, cpl: { border: '#66ff33', icon: '#66ff33' }, docker_host: { border: '#00cc88', icon: '#00cc88' }, docker_container: { border: '#00aacc', icon: '#00aacc' }, @@ -361,6 +371,8 @@ export const THEMES: Record = { camera: { border: '#8b949e', icon: '#8b949e' }, printer: { border: '#8b949e', icon: '#8b949e' }, computer: { border: '#a855f7', icon: '#a855f7' }, + laptop: { border: '#a855f7', icon: '#a855f7' }, + mobile: { border: '#ec4899', icon: '#ec4899' }, cpl: { border: '#e3b341', icon: '#e3b341' }, docker_host: { border: '#2496ED', icon: '#2496ED' }, docker_container: { border: '#0ea5e9', icon: '#0ea5e9' },