Recovery Mode

Your page's code has become invalid. Please choose Revisions on the bottom left to access the last known working state of this page.

import { useCallback, useRef, useState, useEffect } from "react"; import { ReactFlow, addEdge, useNodesState, useEdgesState, Controls, Background, MiniMap, useReactFlow, getNodesBounds, getViewportForBounds, type Connection, type Node, type Edge, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import * as XLSX from "xlsx"; import { toPng } from "html-to-image"; import { Button } from "@/components/ui/button"; import { Network, ChevronDown, Image, FileSpreadsheet, Plus, Users, LayoutTemplate, Pencil, Trash2, MoreVertical, LogOut, Menu, X } from "lucide-react"; import { supabase } from "@/integrations/supabase/client"; import { Switch } from "@/components/ui/switch"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { useIsMobile } from "@/hooks/use-mobile"; import DeviceNode, { type DeviceData } from "@/components/DeviceNode"; import AddDeviceDialog, { type DeviceInfo } from "@/components/AddDeviceDialog"; import EditDeviceDialog, { type EditableDevice } from "@/components/EditDeviceDialog"; const nodeTypes = { device: DeviceNode }; type ClientData = { name: string; devices: (DeviceInfo & { id: string })[]; nodes: Node[]; edges: Edge[]; }; const templateDevices: (DeviceInfo & { id: string })[] = [ { id: "t-isp", name: "Internet / Fibre", ip: "WAN", type: "internet", customFields: {} }, { id: "t-fw1", name: "Firewall", ip: "10.0.0.1", type: "firewall", customFields: {} }, { id: "t-router1", name: "Core Router", ip: "10.0.0.2", type: "router", customFields: {} }, { id: "t-sw1", name: "Switch – Floor 1", ip: "10.0.1.1", type: "switch", customFields: {} }, { id: "t-sw2", name: "Switch – Floor 2", ip: "10.0.2.1", type: "switch", customFields: {} }, { id: "t-srv1", name: "Domain Controller", ip: "10.0.1.10", type: "server", customFields: {} }, { id: "t-nas1", name: "NAS Storage", ip: "10.0.1.15", type: "nas", customFields: {} }, { id: "t-ap1", name: "Wi-Fi AP", ip: "10.0.2.20", type: "accesspoint", customFields: {} }, { id: "t-pc1", name: "Front Desk PC", ip: "DHCP", type: "desktop", customFields: {} }, { id: "t-lt1", name: "Manager Laptop", ip: "DHCP", type: "laptop", customFields: {} }, { id: "t-pr1", name: "Office Printer", ip: "10.0.1.30", type: "printer", customFields: {} }, { id: "t-cam1", name: "Security Camera", ip: "10.0.2.50", type: "camera", customFields: {} }, ]; const templateNodes: Node[] = [ { id: "t-isp", type: "device", position: { x: 300, y: 0 }, data: { label: "Internet / Fibre", ip: "WAN", deviceType: "internet" } }, { id: "t-fw1", type: "device", position: { x: 300, y: 90 }, data: { label: "Firewall", ip: "10.0.0.1", deviceType: "firewall" } }, { id: "t-router1", type: "device", position: { x: 300, y: 180 }, data: { label: "Core Router", ip: "10.0.0.2", deviceType: "router" } }, { id: "t-sw1", type: "device", position: { x: 120, y: 290 }, data: { label: "Switch – Floor 1", ip: "10.0.1.1", deviceType: "switch" } }, { id: "t-sw2", type: "device", position: { x: 480, y: 290 }, data: { label: "Switch – Floor 2", ip: "10.0.2.1", deviceType: "switch" } }, { id: "t-srv1", type: "device", position: { x: 0, y: 410 }, data: { label: "Domain Controller", ip: "10.0.1.10", deviceType: "server" } }, { id: "t-nas1", type: "device", position: { x: 160, y: 410 }, data: { label: "NAS Storage", ip: "10.0.1.15", deviceType: "nas" } }, { id: "t-pr1", type: "device", position: { x: 310, y: 410 }, data: { label: "Office Printer", ip: "10.0.1.30", deviceType: "printer" } }, { id: "t-ap1", type: "device", position: { x: 420, y: 410 }, data: { label: "Wi-Fi AP", ip: "10.0.2.20", deviceType: "accesspoint" } }, { id: "t-cam1", type: "device", position: { x: 570, y: 410 }, data: { label: "Security Camera", ip: "10.0.2.50", deviceType: "camera" } }, { id: "t-pc1", type: "device", position: { x: 370, y: 520 }, data: { label: "Front Desk PC", ip: "DHCP", deviceType: "desktop" } }, { id: "t-lt1", type: "device", position: { x: 520, y: 520 }, data: { label: "Manager Laptop", ip: "DHCP", deviceType: "laptop" } }, ]; const templateEdges: Edge[] = [ { id: "te0", source: "t-isp", target: "t-fw1", animated: true }, { id: "te1", source: "t-fw1", target: "t-router1", animated: true }, { id: "te2", source: "t-router1", target: "t-sw1", animated: true }, { id: "te3", source: "t-router1", target: "t-sw2", animated: true }, { id: "te4", source: "t-sw1", target: "t-srv1", animated: true }, { id: "te5", source: "t-sw1", target: "t-nas1", animated: true }, { id: "te6", source: "t-sw1", target: "t-pr1", animated: true }, { id: "te7", source: "t-sw2", target: "t-ap1", animated: true }, { id: "te8", source: "t-sw2", target: "t-cam1", animated: true }, { id: "te9", source: "t-ap1", target: "t-pc1", animated: true }, { id: "te10", source: "t-ap1", target: "t-lt1", animated: true }, ]; function cloneTemplate() { const idMap: Record = {}; const devices = templateDevices.map((d) => { const newId = `node-${counter++}`; idMap[d.id] = newId; return { ...d, id: newId }; }); const nodes = templateNodes.map((n) => ({ ...n, id: idMap[n.id], data: { ...n.data }, })); const edges = templateEdges.map((e) => ({ ...e, id: `e-${counter++}`, source: idMap[e.source], target: idMap[e.target], })); return { devices, nodes, edges }; } const defaultDevices = templateDevices; const defaultNodes = templateNodes; const defaultEdges = templateEdges; const initialClients: Record = { "demo-client": { name: "Demo Client", devices: [...defaultDevices], nodes: defaultNodes, edges: defaultEdges, }, }; let counter = 10; const STORAGE_KEY = "topoglyph_data"; const ACTIVE_KEY = "topoglyph_active"; function loadFromStorage(): { clients: Record; activeId: string } | null { try { const raw = localStorage.getItem(STORAGE_KEY); const activeId = localStorage.getItem(ACTIVE_KEY); if (raw) { const clients = JSON.parse(raw) as Record; return { clients, activeId: activeId && clients[activeId] ? activeId : Object.keys(clients)[0] }; } } catch { /* ignore */ } return null; } function saveToStorage(clients: Record, activeId: string) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(clients)); localStorage.setItem(ACTIVE_KEY, activeId); } catch { /* ignore */ } } function IndexInner() { const isMobile = useIsMobile(); const [sidebarOpen, setSidebarOpen] = useState(false); const stored = useRef(loadFromStorage()); const [clients, setClients] = useState>(stored.current?.clients ?? initialClients); const [activeClientId, setActiveClientId] = useState(stored.current?.activeId ?? "demo-client"); const [addClientOpen, setAddClientOpen] = useState(false); const [newClientName, setNewClientName] = useState(""); const [useTemplate, setUseTemplate] = useState(true); const activeClient = clients[activeClientId]; const [nodes, setNodes, onNodesChange] = useNodesState(activeClient.nodes); const [edges, setEdges, onEdgesChange] = useEdgesState(activeClient.edges); const devicesRef = useRef<(DeviceInfo & { id: string })[]>([...activeClient.devices]); const canvasRef = useRef(null); const [editDevice, setEditDevice] = useState(null); const [editOpen, setEditOpen] = useState(false); // Auto-save to localStorage useEffect(() => { const updated = { ...clients, [activeClientId]: { ...clients[activeClientId], devices: [...devicesRef.current], nodes: [...nodes], edges: [...edges], }, }; saveToStorage(updated, activeClientId); }, [clients, activeClientId, nodes, edges]); // Save current client state before switching const saveCurrentClient = useCallback(() => { setClients((prev) => ({ ...prev, [activeClientId]: { ...prev[activeClientId], devices: [...devicesRef.current], nodes: [...nodes], edges: [...edges], }, })); }, [activeClientId, nodes, edges]); const switchClient = useCallback((clientId: string) => { // Save current setClients((prev) => { const updated = { ...prev, [activeClientId]: { ...prev[activeClientId], devices: [...devicesRef.current], nodes: [...nodes], edges: [...edges], }, }; // Load new client const newClient = updated[clientId]; devicesRef.current = [...newClient.devices]; setNodes(newClient.nodes); setEdges(newClient.edges); return updated; }); setActiveClientId(clientId); }, [activeClientId, nodes, edges, setNodes, setEdges]); const handleAddClient = () => { if (!newClientName.trim()) return; const id = `client-${Date.now()}`; saveCurrentClient(); let newData: { devices: (DeviceInfo & { id: string })[]; nodes: Node[]; edges: Edge[] }; if (useTemplate) { newData = cloneTemplate(); } else { newData = { devices: [], nodes: [], edges: [] }; } setClients((prev) => ({ ...prev, [id]: { name: newClientName.trim(), ...newData }, })); setActiveClientId(id); devicesRef.current = [...newData.devices]; setNodes(newData.nodes); setEdges(newData.edges); setNewClientName(""); setUseTemplate(true); setAddClientOpen(false); }; const loadDemoTemplate = () => { const { devices, nodes: tNodes, edges: tEdges } = cloneTemplate(); devicesRef.current = [...devicesRef.current, ...devices]; setNodes((nds) => [...nds, ...tNodes]); setEdges((eds) => [...eds, ...tEdges]); }; const onConnect = useCallback( (params: Connection) => setEdges((eds) => addEdge({ ...params, animated: true }, eds)), [setEdges] ); const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => { const device = devicesRef.current.find((d) => d.id === node.id); if (device) { setEditDevice({ ...device }); setEditOpen(true); } }, []); const handleSaveDevice = (updated: EditableDevice) => { devicesRef.current = devicesRef.current.map((d) => (d.id === updated.id ? updated : d)); setNodes((nds) => nds.map((n) => n.id === updated.id ? { ...n, data: { label: updated.name, ip: updated.ip, deviceType: updated.type } as DeviceData } : n ) ); }; const handleDeleteDevice = (id: string) => { devicesRef.current = devicesRef.current.filter((d) => d.id !== id); setNodes((nds) => nds.filter((n) => n.id !== id)); setEdges((eds) => eds.filter((e) => e.source !== id && e.target !== id)); }; const handleAddDevice = (device: DeviceInfo) => { const id = `node-${counter++}`; const newNode: Node = { id, type: "device", position: { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 }, data: { label: device.name, ip: device.ip, deviceType: device.type } as DeviceData, }; setNodes((nds) => [...nds, newNode]); devicesRef.current.push({ ...device, id }); }; const [renameClientId, setRenameClientId] = useState(null); const [renameValue, setRenameValue] = useState(""); const startRenameClient = (id: string) => { setRenameClientId(id); setRenameValue(clients[id].name); }; const confirmRenameClient = () => { if (!renameClientId || !renameValue.trim()) return; setClients((prev) => ({ ...prev, [renameClientId]: { ...prev[renameClientId], name: renameValue.trim() }, })); setRenameClientId(null); }; const deleteClient = (id: string) => { const keys = Object.keys(clients); if (keys.length <= 1) return; // don't delete the last client setClients((prev) => { const copy = { ...prev }; delete copy[id]; return copy; }); if (activeClientId === id) { const remaining = keys.filter((k) => k !== id); const newId = remaining[0]; devicesRef.current = [...clients[newId].devices]; setNodes(clients[newId].nodes); setEdges(clients[newId].edges); setActiveClientId(newId); } }; const exportExcel = () => { const rows = devicesRef.current.map((d) => { const flat: Record = { Name: d.name, IP: d.ip, Type: d.type }; Object.entries(d.customFields).forEach(([k, v]) => { flat[k] = typeof v === "string" ? v : v.value; }); return flat; }); const ws = XLSX.utils.json_to_sheet(rows); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Devices"); XLSX.writeFile(wb, `${activeClient.name.replace(/s+/g, "_")}_devices.xlsx`); }; const exportPNG = async () => { if (!canvasRef.current) return; try { // Inline all computed styles on SVG elements so html-to-image captures edges const svgElements = canvasRef.current.querySelectorAll("svg, path, line, polyline, marker, circle, rect, g"); svgElements.forEach((el) => { const computed = window.getComputedStyle(el); const important = ["stroke", "stroke-width", "stroke-dasharray", "stroke-dashoffset", "fill", "opacity", "marker-end", "marker-start"]; important.forEach((prop) => { const val = computed.getPropertyValue(prop); if (val && val !== "none" && val !== "") { (el as HTMLElement).style.setProperty(prop, val); } }); }); const dataUrl = await toPng(canvasRef.current, { backgroundColor: "#f5f7fa", cacheBust: true, pixelRatio: 2, filter: (node) => { if (node?.classList?.contains("react-flow__minimap")) return false; if (node?.classList?.contains("react-flow__controls")) return false; return true; }, }); const a = document.createElement("a"); a.href = dataUrl; a.download = `${activeClient.name.replace(/s+/g, "_")}_topology.png`; a.click(); } catch (e) { console.error("PNG export failed", e); } }; const sidebarContent = ( <>
Clients
{Object.entries(clients).map(([id, client]) => (
startRenameClient(id)} className="gap-2"> Rename {Object.keys(clients).length > 1 && ( deleteClient(id)} className="gap-2 text-destructive focus:text-destructive"> Delete )}
))}
{/* Rename dialog */} { if (!open) setRenameClientId(null); }}> Rename Client
setRenameValue(e.target.value)} />
Add New Client
setNewClientName(e.target.value)} placeholder="e.g. Acme Corp" />

{useTemplate ? "Pre-populates a typical network (Firewall → Router → Switches → Servers & Workstations). Edit or delete any device." : "Start with an empty canvas."}

); return (
{/* Desktop sidebar */} {!isMobile && ( )} {/* Mobile sidebar as sheet */} {isMobile && ( {sidebarContent} )} {/* Main area */}
{isMobile && ( )}

TopoGlyph

{!isMobile && — {activeClient.name}}
{!isMobile && ( )} {/* Export dropdown */} Image (PNG) Report (Excel) {isMobile && ( Load Demo )}
{!isMobile && ( { const dt = (n.data as DeviceData)?.deviceType; if (dt === "router") return "hsl(215, 80%, 45%)"; if (dt === "switch") return "hsl(260, 60%, 50%)"; if (dt === "server") return "hsl(160, 60%, 42%)"; return "hsl(215, 15%, 50%)"; }} /> )}
); } export default function Index() { return ; }