diff --git a/frontend/src/components/network/nodes/DeviceNode.tsx b/frontend/src/components/network/nodes/DeviceNode.tsx index d0171849..a1f732c0 100644 --- a/frontend/src/components/network/nodes/DeviceNode.tsx +++ b/frontend/src/components/network/nodes/DeviceNode.tsx @@ -1,10 +1,12 @@ import { memo, useState, useRef, useEffect } from 'react' import { Position, NodeResizer, useReactFlow, type NodeProps } from '@xyflow/react' +import { HardDrive, Network, ShieldCheck } from 'lucide-react' import { BaseNode, BaseNodeHeader, BaseNodeContent } from '../ui/base-node' import { BaseHandle } from '../ui/base-handle' import { NodeStatusIndicator, type NodeStatus } from '../ui/node-status-indicator' import { NodeTooltip, NodeTooltipTrigger, NodeTooltipContent } from '../ui/node-tooltip' -import { getDeviceRenderConfig } from './deviceRegistry' +import { getDeviceRenderConfig, CATEGORY_LABELS } from './deviceRegistry' +import { cn } from '@/lib/utils' import type { DeviceProperties } from '@/types' export interface DeviceNodeData { @@ -29,9 +31,48 @@ const NODE_DEFAULT = 120 // default square side in px const NODE_MIN = 80 // minimum square side in px const NODE_MAX = 280 // maximum square side in px +function DeviceNodeChrome({ + category, + accentClass, + glyph, +}: { + category: string + accentClass: string + glyph: string +}) { + const MetaIcon = category === 'network' + ? Network + : category === 'security' + ? ShieldCheck + : HardDrive + + return ( + <> +
+ + {glyph} + + + {CATEGORY_LABELS[category] ?? category} + +
+
+
+ + Asset +
+
+ + + +
+ + ) +} + function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) { const nodeData = data as unknown as DeviceNodeData - const { icon: Icon, color } = getDeviceRenderConfig(nodeData.deviceType, nodeData.category) + const { icon: Icon, color, accentClass, surfaceClass, category, glyph } = getDeviceRenderConfig(nodeData.deviceType, nodeData.category) const status = (nodeData.properties?.status || 'unknown') as NodeStatus const ip = nodeData.properties?.ip const props = nodeData.properties || {} @@ -46,6 +87,8 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) { const labelPx = Math.max(9, Math.min(20, Math.round(scale * 11))) // IP font: 9px at default, clamped to [8, 16] const ipPx = Math.max(8, Math.min(16, Math.round(scale * 9))) + const typePx = Math.max(8, Math.min(13, Math.round(scale * 8.5))) + const iconPlateSize = Math.round(Math.max(36, Math.min(92, scale * 56))) const [editing, setEditing] = useState(false) const [labelValue, setLabelValue] = useState(nodeData.label ?? '') @@ -81,9 +124,23 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) { - - - + +
+ + +
+
+
+
+ +
+
{editing ? ( { e.stopPropagation() setEditing(true) @@ -118,10 +175,21 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) { {labelValue} )} + + {nodeData.deviceType.replace(/-/g, ' ')} + {ip && ( - - {ip} + + + {ip} + )} diff --git a/frontend/src/components/network/nodes/deviceRegistry.ts b/frontend/src/components/network/nodes/deviceRegistry.ts index 7dec611d..c2385fe2 100644 --- a/frontend/src/components/network/nodes/deviceRegistry.ts +++ b/frontend/src/components/network/nodes/deviceRegistry.ts @@ -9,6 +9,10 @@ import { export interface DeviceRenderConfig { icon: LucideIcon color: string + accentClass: string + surfaceClass: string + category: string + glyph: string } // Category-semantic color palette — each color carries meaning: @@ -27,62 +31,109 @@ export const STORAGE_COLOR = '#a78bfa' export const CLOUD_COLOR = '#67e8f9' export const INFRA_COLOR = '#94a3b8' +const CATEGORY_STYLES: Record> = { + network: { + accentClass: 'border-sky-400/40 bg-sky-400/12 text-sky-300', + surfaceClass: 'from-sky-400/12 via-sky-400/4 to-transparent', + }, + security: { + accentClass: 'border-rose-400/40 bg-rose-400/12 text-rose-300', + surfaceClass: 'from-rose-400/12 via-rose-400/4 to-transparent', + }, + compute: { + accentClass: 'border-emerald-400/40 bg-emerald-400/12 text-emerald-300', + surfaceClass: 'from-emerald-400/12 via-emerald-400/4 to-transparent', + }, + storage: { + accentClass: 'border-violet-400/40 bg-violet-400/12 text-violet-300', + surfaceClass: 'from-violet-400/12 via-violet-400/4 to-transparent', + }, + cloud: { + accentClass: 'border-cyan-400/40 bg-cyan-400/12 text-cyan-300', + surfaceClass: 'from-cyan-400/12 via-cyan-400/4 to-transparent', + }, + endpoint: { + accentClass: 'border-amber-400/40 bg-amber-400/12 text-amber-300', + surfaceClass: 'from-amber-400/12 via-amber-400/4 to-transparent', + }, + infrastructure: { + accentClass: 'border-slate-400/40 bg-slate-300/10 text-slate-300', + surfaceClass: 'from-slate-300/10 via-slate-300/4 to-transparent', + }, +} + +function makeConfig( + icon: LucideIcon, + color: string, + category: string, + glyph: string, +): DeviceRenderConfig { + return { + icon, + color, + category, + glyph, + accentClass: CATEGORY_STYLES[category]?.accentClass ?? CATEGORY_STYLES.infrastructure.accentClass, + surfaceClass: CATEGORY_STYLES[category]?.surfaceClass ?? CATEGORY_STYLES.infrastructure.surfaceClass, + } +} + const SYSTEM_DEVICE_ICONS: Record = { // Network layer - 'router': { icon: Router, color: NETWORK_COLOR }, - 'switch': { icon: Network, color: NETWORK_COLOR }, - 'access-point': { icon: Wifi, color: NETWORK_COLOR }, - 'load-balancer': { icon: Gauge, color: NETWORK_COLOR }, + 'router': makeConfig(Router, NETWORK_COLOR, 'network', 'RTR'), + 'switch': makeConfig(Network, NETWORK_COLOR, 'network', 'SW'), + 'access-point': makeConfig(Wifi, NETWORK_COLOR, 'network', 'AP'), + 'load-balancer': makeConfig(Gauge, NETWORK_COLOR, 'network', 'LB'), // Security - 'firewall': { icon: BrickWallFire, color: SECURITY_COLOR }, - 'badge-reader': { icon: KeyRound, color: SECURITY_COLOR }, + 'firewall': makeConfig(BrickWallFire, SECURITY_COLOR, 'security', 'FW'), + 'badge-reader': makeConfig(KeyRound, SECURITY_COLOR, 'security', 'BR'), // Compute - 'server': { icon: Server, color: COMPUTE_COLOR }, - 'vm': { icon: Boxes, color: COMPUTE_COLOR }, - 'container': { icon: Package, color: COMPUTE_COLOR }, + 'server': makeConfig(Server, COMPUTE_COLOR, 'compute', 'SRV'), + 'vm': makeConfig(Boxes, COMPUTE_COLOR, 'compute', 'VM'), + 'container': makeConfig(Package, COMPUTE_COLOR, 'compute', 'CTR'), // Storage - 'nas': { icon: Database, color: STORAGE_COLOR }, - 'san': { icon: HardDrive, color: STORAGE_COLOR }, - 'cloud-storage': { icon: CloudCog, color: STORAGE_COLOR }, + 'nas': makeConfig(Database, STORAGE_COLOR, 'storage', 'NAS'), + 'san': makeConfig(HardDrive, STORAGE_COLOR, 'storage', 'SAN'), + 'cloud-storage': makeConfig(CloudCog, STORAGE_COLOR, 'storage', 'CS'), // Cloud / Internet - 'cloud': { icon: Cloud, color: CLOUD_COLOR }, - 'aws': { icon: Cloud, color: CLOUD_COLOR }, - 'azure': { icon: Cloud, color: CLOUD_COLOR }, - 'gcp': { icon: Cloud, color: CLOUD_COLOR }, - 'isp': { icon: Globe, color: CLOUD_COLOR }, + 'cloud': makeConfig(Cloud, CLOUD_COLOR, 'cloud', 'CLD'), + 'aws': makeConfig(Cloud, CLOUD_COLOR, 'cloud', 'AWS'), + 'azure': makeConfig(Cloud, CLOUD_COLOR, 'cloud', 'AZ'), + 'gcp': makeConfig(Cloud, CLOUD_COLOR, 'cloud', 'GCP'), + 'isp': makeConfig(Globe, CLOUD_COLOR, 'cloud', 'WAN'), // Endpoints - 'workstation': { icon: Monitor, color: ENDPOINT_COLOR }, - 'laptop': { icon: Laptop, color: ENDPOINT_COLOR }, - 'tablet': { icon: Tablet, color: ENDPOINT_COLOR }, - 'phone': { icon: Smartphone, color: ENDPOINT_COLOR }, - 'printer': { icon: Printer, color: ENDPOINT_COLOR }, + 'workstation': makeConfig(Monitor, ENDPOINT_COLOR, 'endpoint', 'WS'), + 'laptop': makeConfig(Laptop, ENDPOINT_COLOR, 'endpoint', 'LTP'), + 'tablet': makeConfig(Tablet, ENDPOINT_COLOR, 'endpoint', 'TAB'), + 'phone': makeConfig(Smartphone, ENDPOINT_COLOR, 'endpoint', 'PH'), + 'printer': makeConfig(Printer, ENDPOINT_COLOR, 'endpoint', 'PRN'), // Infrastructure / physical - 'ups': { icon: BatteryCharging, color: INFRA_COLOR }, - 'pdu': { icon: PlugZap, color: INFRA_COLOR }, - 'rack': { icon: RectangleVertical, color: INFRA_COLOR }, - 'patch-panel': { icon: Cable, color: INFRA_COLOR }, - 'camera': { icon: Camera, color: INFRA_COLOR }, - 'nvr': { icon: Video, color: INFRA_COLOR }, - 'iot': { icon: Radio, color: INFRA_COLOR }, + 'ups': makeConfig(BatteryCharging, INFRA_COLOR, 'infrastructure', 'UPS'), + 'pdu': makeConfig(PlugZap, INFRA_COLOR, 'infrastructure', 'PDU'), + 'rack': makeConfig(RectangleVertical, INFRA_COLOR, 'infrastructure', 'RCK'), + 'patch-panel': makeConfig(Cable, INFRA_COLOR, 'infrastructure', 'PP'), + 'camera': makeConfig(Camera, INFRA_COLOR, 'infrastructure', 'CAM'), + 'nvr': makeConfig(Video, INFRA_COLOR, 'infrastructure', 'NVR'), + 'iot': makeConfig(Radio, INFRA_COLOR, 'infrastructure', 'IOT'), } const CATEGORY_DEFAULTS: Record = { - 'network': { icon: Router, color: NETWORK_COLOR }, - 'compute': { icon: Server, color: COMPUTE_COLOR }, - 'storage': { icon: Database, color: STORAGE_COLOR }, - 'cloud': { icon: Cloud, color: CLOUD_COLOR }, - 'endpoint': { icon: Monitor, color: ENDPOINT_COLOR }, - 'infrastructure': { icon: PlugZap, color: INFRA_COLOR }, - 'security': { icon: BrickWallFire, color: SECURITY_COLOR }, + 'network': makeConfig(Router, NETWORK_COLOR, 'network', 'NET'), + 'compute': makeConfig(Server, COMPUTE_COLOR, 'compute', 'CPU'), + 'storage': makeConfig(Database, STORAGE_COLOR, 'storage', 'DB'), + 'cloud': makeConfig(Cloud, CLOUD_COLOR, 'cloud', 'CLD'), + 'endpoint': makeConfig(Monitor, ENDPOINT_COLOR, 'endpoint', 'END'), + 'infrastructure': makeConfig(PlugZap, INFRA_COLOR, 'infrastructure', 'INF'), + 'security': makeConfig(BrickWallFire, SECURITY_COLOR, 'security', 'SEC'), } -const FALLBACK: DeviceRenderConfig = { icon: Cpu, color: INFRA_COLOR } +const FALLBACK: DeviceRenderConfig = makeConfig(Cpu, INFRA_COLOR, 'infrastructure', 'DEV') export function getDeviceRenderConfig(slug: string, category?: string): DeviceRenderConfig { if (SYSTEM_DEVICE_ICONS[slug]) return SYSTEM_DEVICE_ICONS[slug] diff --git a/frontend/src/components/network/ui/base-node.tsx b/frontend/src/components/network/ui/base-node.tsx index 7a45868f..a6e8f955 100644 --- a/frontend/src/components/network/ui/base-node.tsx +++ b/frontend/src/components/network/ui/base-node.tsx @@ -5,8 +5,8 @@ export function BaseNode({ className, ...props }: ComponentProps<'div'>) { return (
+ chip: string +} + +const nodes: NodeSpec[] = [ + { id: 'wan', label: 'ISP Edge', meta: 'wan uplink', x: 240, y: 28, accent: '#67e8f9', Icon: Cloud, chip: 'WAN' }, + { id: 'fw', label: 'Perimeter FW', meta: 'ha pair', x: 240, y: 150, accent: '#f87171', Icon: Shield, chip: 'FW' }, + { id: 'core', label: 'Core Switch', meta: 'stacked', x: 240, y: 278, accent: '#60a5fa', Icon: Network, chip: 'SW' }, + { id: 'ap', label: 'Access Point', meta: 'wifi 6', x: 72, y: 406, accent: '#60a5fa', Icon: Wifi, chip: 'AP' }, + { id: 'srv', label: 'File Server', meta: 'vm host', x: 240, y: 406, accent: '#34d399', Icon: Server, chip: 'SRV' }, + { id: 'nas', label: 'Backup NAS', meta: 'snapshots', x: 408, y: 406, accent: '#a78bfa', Icon: Database, chip: 'NAS' }, + { id: 'desk', label: 'User Workstation', meta: 'accounting', x: 240, y: 534, accent: '#fbbf24', Icon: Monitor, chip: 'WS' }, +] + +const links = [ + ['wan', 'fw'], + ['fw', 'core'], + ['core', 'ap'], + ['core', 'srv'], + ['core', 'nas'], + ['core', 'desk'], +] as const + +function ApplianceNode({ node }: { node: NodeSpec }) { + const { Icon } = node + return ( +
+
+ + {node.chip} + + Asset +
+
+
+ +
+
{node.label}
+
{node.meta}
+
+
+ + + +
+
+ ) +} + +function SaaSNode({ node }: { node: NodeSpec }) { + const { Icon } = node + return ( +
+
+
+ +
+
{node.label}
+
{node.meta}
+
+
+ ) +} + +function DiagramWires({ variant }: { variant: 'appliance' | 'saas' }) { + return ( + + {links.map(([from, to]) => { + const source = nodes.find(node => node.id === from) + const target = nodes.find(node => node.id === to) + if (!source || !target) return null + + const x1 = source.x + 68 + const y1 = source.y + (variant === 'appliance' ? 108 : 100) + const x2 = target.x + 68 + const y2 = target.y + const midY = (y1 + y2) / 2 + + return ( + + ) + })} + + ) +} + +function DiagramPanel({ + title, + subtitle, + description, + variant, +}: { + title: string + subtitle: string + description: string + variant: 'appliance' | 'saas' +}) { + return ( +
+
+
+ {subtitle} +
+

{title}

+

{description}

+
+
+
+
+
+
+ + {nodes.map(node => + variant === 'appliance' ? ( + + ) : ( + + ), + )} +
+
+
+
+ ) +} + +export default function AssetStyleShowcasePage() { + const navigate = useNavigate() + + return ( +
+
+
+ +
+ Asset Style Lab +
+

Two directions for network-map assets

+

+ Same topology, two visual languages. The left mock-up leans closer to infrastructure documentation. + The right mock-up leans toward a cleaner product experience inside the app. +

+
+
+ +
+ + +
+ +
+ The fastest next step is to pick the lane that feels more “ResolutionFlow,” then I can translate that look into the actual editor nodes rather than keeping it as a mock-up. +
+
+ ) +} diff --git a/frontend/src/pages/NetworkDiagrams/index.tsx b/frontend/src/pages/NetworkDiagrams/index.tsx index 4ef87e27..c60cf732 100644 --- a/frontend/src/pages/NetworkDiagrams/index.tsx +++ b/frontend/src/pages/NetworkDiagrams/index.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useNavigate } from 'react-router-dom' -import { Plus, Search, Network, MoreHorizontal, Upload, ChevronDown } from 'lucide-react' +import { Plus, Search, Network, MoreHorizontal, Upload, ChevronDown, Sparkles } from 'lucide-react' import { cn } from '@/lib/utils' import { networkDiagramsApi } from '@/api' import { toast } from '@/lib/toast' @@ -171,6 +171,13 @@ export default function NetworkDiagramsPage() {

Visual network topology documentation for your clients

+