"use client"; import Shell from "@/components/Shell"; import TrafficChart from "@/components/TrafficChart"; import { apiFetch, getCsrfFromCookie, WS_BASE } from "@/lib/api"; import { useEffect, useMemo, useRef, useState } from "react"; type Panel = { id:number; title:string; router_id:number; config:any }; export default function DashboardView({ params }: { params: { id: string } }) { const dashboardId = Number(params.id); const [panels, setPanels] = useState([]); const [dashName, setDashName] = useState(""); const [err, setErr] = useState(null); // create panel form const [title, setTitle] = useState("WAN"); const [routerId, setRouterId] = useState(1); const [interfaces, setInterfaces] = useState("ether1"); const [metrics, setMetrics] = useState("rx_bps,tx_bps"); const [intervalMs, setIntervalMs] = useState(1000); const [windowPts, setWindowPts] = useState(120); const [routers, setRouters] = useState([]); const dataMap = useRef>(new Map()); // panelId -> points async function load() { setErr(null); try { const d = await apiFetch(`/api/dashboards/${dashboardId}`); setDashName(d.dashboard.name); setPanels(d.panels); } catch (e:any) { setErr(e.message || "error"); } } async function loadRouters() { try { const r = await apiFetch("/api/routers"); setRouters(r); if (r?.length) setRouterId(r[0].id); } catch {} } useEffect(() => { load(); loadRouters(); }, [dashboardId]); async function createPanel() { setErr(null); try { const csrf = getCsrfFromCookie(); const cfg = { interfaces: interfaces.split(",").map(s=>s.trim()).filter(Boolean), metrics: metrics.split(",").map(s=>s.trim()).filter(Boolean), interval_ms: intervalMs, window: windowPts }; await apiFetch(`/api/dashboards/${dashboardId}/panels`, { method: "POST", headers: { "X-CSRF-Token": csrf || "" }, body: JSON.stringify({ title, router_id: routerId, config: cfg }) }); await load(); } catch (e:any) { setErr(e.message || "error"); } } return (
{dashName || "Dashboard"}
Live charts
{err &&
{err}
}
Add panel
setTitle(e.target.value)} placeholder="title"/> setInterfaces(e.target.value)} placeholder="interfaces: ether1,ether2"/> setMetrics(e.target.value)} placeholder="metrics: rx_bps,tx_bps"/>
interval ms setIntervalMs(Number(e.target.value))}/>
window pts setWindowPts(Number(e.target.value))}/>
{panels.map(p => ( ))}
); } function PanelCard({ panel, dataMapRef }: { panel: Panel; dataMapRef: any }) { const [data, setData] = useState([]); const wsRef = useRef(null); const series = useMemo(() => { const cfg = panel.config || {}; const ifs: string[] = cfg.interfaces || []; const ms: string[] = cfg.metrics || ["rx_bps","tx_bps"]; const out: string[] = []; for (const i of (ifs.length ? ifs : ["*"])) { for (const m of ms) out.push(`${i}:${m}`); } return out; }, [panel]); useEffect(() => { let stop = false; const windowPts = Number(panel.config?.window || 120); const ws = new WebSocket(`${WS_BASE.replace("http","ws")}/ws/stream`); wsRef.current = ws; ws.onopen = () => { ws.send(JSON.stringify({ panelId: panel.id })); }; ws.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === "traffic" && msg.panelId === panel.id) { const rows = msg.data || []; const now = new Date(); const t = now.toLocaleTimeString(); const points = (dataMapRef.current.get(panel.id) || []) as any[]; // multi-series: iface:metric const pt: any = { t }; for (const r of rows) { const iface = r.iface; pt[`${iface}:rx_bps`] = Number(r.rx_bps || 0); pt[`${iface}:tx_bps`] = Number(r.tx_bps || 0); } points.push(pt); while (points.length > windowPts) points.shift(); dataMapRef.current.set(panel.id, points); if (!stop) setData([...points]); } } catch {} }; const ping = setInterval(() => { try { ws.send("ping"); } catch {} }, 15000); return () => { stop = true; clearInterval(ping); try { ws.close(); } catch {} }; }, [panel.id]); return (
{panel.title}
panelId: {panel.id} • routerId: {panel.router_id}
s !== "*:rx_bps" && s !== "*:tx_bps")} />
); }