/** * SwitchPortManager -- Visual switch port layout with VLAN color coding. * * Displays physical ethernet ports in a horizontal grid resembling a * physical switch front panel. Each port shows link status, speed, and * VLAN assignment with color-coded border stripes. Clicking a port * opens a detail popover (read-only). */ import { useMemo } from 'react' import { Zap, Network } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { useConfigBrowse } from '@/hooks/useConfigPanel' import type { ConfigPanelProps } from '@/lib/configPanelTypes' import { cn } from '@/lib/utils' // --------------------------------------------------------------------------- // VLAN color palette (uses chart CSS variable indices) // --------------------------------------------------------------------------- const VLAN_COLORS = [ 'hsl(var(--chart-1))', 'hsl(var(--chart-2))', 'hsl(var(--chart-3))', 'hsl(var(--chart-4))', 'hsl(var(--chart-5))', 'hsl(var(--chart-6, 280 65% 60%))', // fallback if chart-6 not defined ] const UNASSIGNED_COLOR = 'hsl(var(--border))' // --------------------------------------------------------------------------- // Speed helpers // --------------------------------------------------------------------------- function parseSpeed(entry: Record): string { // Try the 'rate' or 'speed' property first (CRS switches expose this) const speed = entry.speed || entry.rate || '' if (speed) { // RouterOS may return e.g. "1Gbps", "10Gbps", "100Mbps" if (/10[gG]/.test(speed)) return '10G' if (/1[gG]/.test(speed)) return '1G' if (/100[mM]/.test(speed)) return '100M' if (/10[mM]/.test(speed)) return '10M' return speed } // Fallback: try to infer from actual-mtu or name const mtu = Number(entry['actual-mtu'] || entry.mtu || 0) if (mtu >= 9000) return '10G' return '---' } // --------------------------------------------------------------------------- // SwitchPortManager // --------------------------------------------------------------------------- export function SwitchPortManager({ tenantId, deviceId, active }: ConfigPanelProps) { // Browse interfaces and bridge port assignments const interfaces = useConfigBrowse(tenantId, deviceId, '/interface', { enabled: active }) const bridgePorts = useConfigBrowse(tenantId, deviceId, '/interface/bridge/port', { enabled: active, }) const vlans = useConfigBrowse(tenantId, deviceId, '/interface/vlan', { enabled: active }) // Filter to only ethernet interfaces const etherPorts = useMemo(() => { return interfaces.entries .filter( (e) => (e.type || '').toLowerCase().includes('ether') || (e.name || '').toLowerCase().startsWith('ether') || (e.name || '').toLowerCase().startsWith('sfp'), ) .sort((a, b) => { // Natural sort: ether1, ether2, ..., ether10, sfp1, etc. const nameA = a.name || '' const nameB = b.name || '' const numA = parseInt(nameA.replace(/\D/g, ''), 10) || 0 const numB = parseInt(nameB.replace(/\D/g, ''), 10) || 0 if (nameA.startsWith('sfp') && !nameB.startsWith('sfp')) return 1 if (!nameA.startsWith('sfp') && nameB.startsWith('sfp')) return -1 return numA - numB }) }, [interfaces.entries]) // Build VLAN color map: pvid -> color index const { vlanColorMap, vlanLegend } = useMemo(() => { const pvidSet = new Set() for (const bp of bridgePorts.entries) { if (bp.pvid && bp.pvid !== '1') { pvidSet.add(bp.pvid) } } // Also include VLANs from the VLAN interface list for (const v of vlans.entries) { if (v['vlan-id']) { pvidSet.add(v['vlan-id']) } } const colorMap = new Map() const legend: { id: string; name: string; color: string }[] = [] let colorIdx = 0 for (const pid of Array.from(pvidSet).sort((a, b) => Number(a) - Number(b))) { const color = VLAN_COLORS[colorIdx % VLAN_COLORS.length] colorMap.set(pid, color) // Find VLAN name if available const vlanEntry = vlans.entries.find((v) => v['vlan-id'] === pid) legend.push({ id: pid, name: vlanEntry?.name || `VLAN ${pid}`, color, }) colorIdx++ } return { vlanColorMap: colorMap, vlanLegend: legend } }, [bridgePorts.entries, vlans.entries]) // Map interface name -> bridge port entry for quick lookup const portAssignments = useMemo(() => { const map = new Map>() for (const bp of bridgePorts.entries) { if (bp.interface) { map.set(bp.interface, bp) } } return map }, [bridgePorts.entries]) const isLoading = interfaces.isLoading || bridgePorts.isLoading if (isLoading) { return (
{Array.from({ length: 8 }).map((_, i) => ( ))}
) } if (etherPorts.length === 0) { return (
No ethernet ports detected on this device.
) } return (
{/* Port grid */}

Switch Ports

{etherPorts.map((port) => { const assignment = portAssignments.get(port.name) const pvid = assignment?.pvid const vlanColor = pvid && pvid !== '1' ? vlanColorMap.get(pvid) : undefined return ( ) })}
{/* VLAN Legend */}

VLAN Legend

{vlanLegend.map((item) => ( ))}
) } // --------------------------------------------------------------------------- // PortCard // --------------------------------------------------------------------------- function PortCard({ port, assignment, vlanColor, speed, }: { port: Record assignment: Record | undefined vlanColor: string speed: string }) { const isRunning = port.running === 'true' const isDisabled = port.disabled === 'true' const isUp = isRunning && !isDisabled const portName = port.name || '---' // PoE heuristic: ports that include "poe" in name or device has PoE capability const hasPoe = portName.toLowerCase().includes('poe') || portName.toLowerCase().startsWith('ether') return ( ) } // --------------------------------------------------------------------------- // Port Detail Popover Content // --------------------------------------------------------------------------- function PortDetail({ port, assignment, speed, }: { port: Record assignment: Record | undefined speed: string }) { return (

{port.name}

{port.disabled === 'true' ? 'Disabled' : port.running === 'true' ? 'Up' : 'Down'}
MAC Address {port['mac-address'] || '---'} Speed {speed} MTU {port.mtu || port['actual-mtu'] || '---'} Type {port.type || '---'} {assignment && ( <> Bridge {assignment.bridge || '---'} PVID {assignment.pvid || '1'} )}

Edit in Interfaces tab

) } // --------------------------------------------------------------------------- // Legend Item // --------------------------------------------------------------------------- function LegendItem({ color, label }: { color: string; label: string }) { return (
{label}
) }