import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { Plus, Search, Network, MoreHorizontal, Upload, ChevronDown, FileJson, FileOutput } from 'lucide-react' import { cn } from '@/lib/utils' import { networkDiagramsApi } from '@/api' import { toast } from '@/lib/toast' import { CATEGORY_COLORS } from '@/components/network/nodes/deviceRegistry' import type { NetworkDiagramListItem, DiagramImportData } from '@/types' const OTHER_COLOR = '#4f5666' function TopologyBar({ categoryCounts, nodeCount }: { categoryCounts: Record; nodeCount: number }) { if (nodeCount === 0) return null const sorted = Object.entries(categoryCounts).sort(([, a], [, b]) => b - a) const tooltipLabel = sorted.map(([cat, count]) => `${count} ${cat}`).join(' · ') return (
{sorted.map(([cat, count]) => (
))}
) } export default function NetworkDiagramsPage() { const navigate = useNavigate() const [diagrams, setDiagrams] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [clientFilter, setClientFilter] = useState(null) const [clientOptions, setClientOptions] = useState([]) const [clientDropdownOpen, setClientDropdownOpen] = useState(false) const [clientSearch, setClientSearch] = useState('') const [menuOpenId, setMenuOpenId] = useState(null) const [confirmArchiveId, setConfirmArchiveId] = useState(null) const [importMenuOpen, setImportMenuOpen] = useState(false) const clientDropdownRef = useRef(null) const importMenuRef = useRef(null) const drawioListImportRef = useRef(null) useEffect(() => { if (!clientDropdownOpen) return const handleClick = (e: MouseEvent) => { if (clientDropdownRef.current && !clientDropdownRef.current.contains(e.target as Node)) { setClientDropdownOpen(false) } } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, [clientDropdownOpen]) useEffect(() => { if (!importMenuOpen) return const handleClick = (e: MouseEvent) => { if (importMenuRef.current && !importMenuRef.current.contains(e.target as Node)) { setImportMenuOpen(false) } } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, [importMenuOpen]) const loadDiagrams = useCallback(async () => { try { const params: Record = {} if (clientFilter) params.client_name = clientFilter if (search) params.search = search const data = await networkDiagramsApi.list(params) setDiagrams(data) } catch { toast.error('Failed to load diagrams') } finally { setLoading(false) } }, [clientFilter, search]) const loadClients = useCallback(async () => { try { const clients = await networkDiagramsApi.listClients() setClientOptions(clients) } catch { /* ignore */ } }, []) useEffect(() => { loadDiagrams() }, [loadDiagrams]) useEffect(() => { loadClients() }, [loadClients]) const filteredClients = useMemo(() => { if (!clientSearch) return clientOptions const lower = clientSearch.toLowerCase() return clientOptions.filter(c => c.toLowerCase().includes(lower)) }, [clientOptions, clientSearch]) const handleDuplicate = useCallback(async (id: string) => { try { const dup = await networkDiagramsApi.duplicate(id) toast.success(`Created: ${dup.name}`) loadDiagrams() } catch { toast.error('Failed to duplicate') } setMenuOpenId(null) }, [loadDiagrams]) const handleArchive = useCallback(async (id: string) => { try { await networkDiagramsApi.archive(id) toast.success('Diagram archived') loadDiagrams() } catch { toast.error('Failed to archive') } setMenuOpenId(null) setConfirmArchiveId(null) }, [loadDiagrams]) const handleImport = useCallback(async () => { const input = document.createElement('input') input.type = 'file' input.accept = '.json' input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0] if (!file) return try { const text = await file.text() const data = JSON.parse(text) as DiagramImportData const result = await networkDiagramsApi.importJson(data) if (result.warnings.length > 0) { toast.warning(`Imported with warnings: ${result.warnings.join(', ')}`) } else { toast.success('Diagram imported') } navigate(`/network-diagrams/${result.diagram.id}`) } catch { toast.error('Failed to import — check that the file is a valid diagram JSON') } } input.click() }, [navigate]) const handleListDrawioImport = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return e.target.value = '' try { const { parseDrawioXml } = await import('@/lib/drawio-import') const text = await file.text() const { nodes: importedNodes, edges: importedEdges, warnings } = parseDrawioXml(text) const result = await networkDiagramsApi.importJson({ schemaVersion: 1, name: file.name.replace(/\.drawio$/i, '') || 'Imported Diagram', client_name: null, description: null, nodes: importedNodes, edges: importedEdges, }) const allWarnings = [...warnings, ...result.warnings] if (allWarnings.length > 0) { toast.warning(`Imported with ${allWarnings.length} warning(s)`) } else { toast.success('Imported successfully') } navigate(`/network-diagrams/${result.diagram.id}`) } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown error' toast.error(`Import failed: ${msg}`) } }, [navigate]) const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } return (

Network Maps

Visual network topology documentation for your clients

{/* Single "Import" dropdown replacing two separate buttons */}
{importMenuOpen && (
)}
setSearch(e.target.value)} className="w-full rounded border border-default bg-input pl-9 pr-3 py-2 text-sm text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none" />
{clientDropdownOpen && (
setClientSearch(e.target.value)} className="w-full rounded border border-default bg-input px-2 py-1 text-xs text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none" autoFocus />
{filteredClients.map(c => ( ))}
)}
{loading && (
{[1, 2, 3].map(i => (
))}
)} {!loading && diagrams.length === 0 && (
{/* Left: mini topology preview */}
{/* Dot grid background */} {/* Static topology SVG */} {/* Edges */} {/* Firewall node */} Firewall {/* Switch node */} Core Switch {/* Router node */} Router {/* Server farm */} Servers {/* Leaf nodes */} PC × 12 AP × 4 NAS VM × 6
{/* Right: value prop + CTA */}
Network Maps

Document every client's infrastructure — once

Drag-and-drop topology diagrams that live next to your tickets. Generate a first draft from a plain-text description, then keep it up to date as networks change.

  • AI topology generation from natural language
  • Export to PNG, SVG, PDF, or draw.io
  • Shared across your whole team instantly
)} {!loading && diagrams.length > 0 && (
{diagrams.map(d => (
navigate(`/network-diagrams/${d.id}`)} className="group relative cursor-pointer rounded-lg border border-default bg-card p-4 hover:border-hover" >

{d.name}

{d.client_name && ( {d.client_name} )} {d.description && (

{d.description}

)} {/* Thumbnail preview */} {d.thumbnail_url ? (
{d.name} { (e.target as HTMLImageElement).style.display = 'none' }} />
) : (
)} {d.node_count > 0 && (
)}
{d.node_count} device{d.node_count !== 1 ? 's' : ''} {formatDate(d.created_at)}
{menuOpenId === d.id && (
{confirmArchiveId === d.id ? ( <>

Archive this diagram?

) : ( <> )}
)}
))}
)}
) }