From 2622258b04054d832f4c74a1e4d5cafd1cf1f22a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sat, 4 Apr 2026 07:57:43 +0000 Subject: [PATCH] feat: add Network Diagrams list page with search, client filter, import Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/NetworkDiagrams/index.tsx | 260 +++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 frontend/src/pages/NetworkDiagrams/index.tsx diff --git a/frontend/src/pages/NetworkDiagrams/index.tsx b/frontend/src/pages/NetworkDiagrams/index.tsx new file mode 100644 index 00000000..51bf9f55 --- /dev/null +++ b/frontend/src/pages/NetworkDiagrams/index.tsx @@ -0,0 +1,260 @@ +import { useState, useEffect, useCallback, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' +import { Plus, Search, Network, MoreHorizontal, Upload } from 'lucide-react' +import { cn } from '@/lib/utils' +import { networkDiagramsApi } from '@/api' +import { toast } from '@/lib/toast' +import type { NetworkDiagramListItem, DiagramImportData } from '@/types' + +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 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) + }, [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 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

+
+
+ + +
+
+ +
+
+ + 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 && ( +
+ +

No network maps yet

+

Create your first network diagram to get started

+ +
+ )} + + {!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}

+ )} +
+ {d.node_count} device{d.node_count !== 1 ? 's' : ''} + {formatDate(d.created_at)} +
+ + {menuOpenId === d.id && ( +
+ + + +
+ )} +
+ ))} +
+ )} +
+ ) +}