From e833d44c43f1f44b78644feecdac7f34e27d5bcc Mon Sep 17 00:00:00 2001 From: xiaomiku01 Date: Sat, 11 Apr 2026 00:41:09 +0800 Subject: [PATCH] feat(ui): add network probes table and latency chart section Displays probe list with protocol badges, latency/loss stats, and delete functionality. Includes a latency line chart using ChartCard with data sourced from the network-probe-stats API. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../routes/system/network-probes.tsx | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 internal/site/src/components/routes/system/network-probes.tsx diff --git a/internal/site/src/components/routes/system/network-probes.tsx b/internal/site/src/components/routes/system/network-probes.tsx new file mode 100644 index 00000000..b0ef6e9c --- /dev/null +++ b/internal/site/src/components/routes/system/network-probes.tsx @@ -0,0 +1,238 @@ +import { useCallback, useEffect, useMemo, useState } from "react" +import { Trans, useLingui } from "@lingui/react/macro" +import { pb } from "@/lib/api" +import { useStore } from "@nanostores/react" +import { $chartTime } from "@/lib/stores" +import { chartTimeData, cn, toFixedFloat, decimalString } from "@/lib/utils" +import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Trash2Icon } from "lucide-react" +import { useToast } from "@/components/ui/use-toast" +import { AddProbeDialog } from "./probe-dialog" +import { ChartCard } from "./chart-card" +import LineChartDefault, { type DataPoint } from "@/components/charts/line-chart" +import { pinnedAxisDomain } from "@/components/ui/chart" +import type { ChartData, NetworkProbeRecord, NetworkProbeStatsRecord } from "@/types" + +export default function NetworkProbes({ + systemId, + chartData, + grid, +}: { + systemId: string + chartData: ChartData + grid: boolean +}) { + const [probes, setProbes] = useState([]) + const [stats, setStats] = useState([]) + const [latestResults, setLatestResults] = useState>({}) + const chartTime = useStore($chartTime) + const { toast } = useToast() + const { t } = useLingui() + + const fetchProbes = useCallback(() => { + pb.send("/api/beszel/network-probes", { + query: { system: systemId }, + }) + .then(setProbes) + .catch(() => setProbes([])) + }, [systemId]) + + useEffect(() => { + fetchProbes() + }, [fetchProbes]) + + // Fetch probe stats based on chart time + useEffect(() => { + if (probes.length === 0) return + const controller = new AbortController() + const statsType = chartTimeData[chartTime]?.type ?? "1m" + + pb.send<{ stats: any; created: string }[]>("/api/beszel/network-probe-stats", { + query: { system: systemId, type: statsType }, + signal: controller.signal, + }) + .then((raw) => { + const data: NetworkProbeStatsRecord[] = raw.map((r) => ({ + stats: r.stats, + created: new Date(r.created).getTime(), + })) + setStats(data) + if (data.length > 0) { + const last = data[data.length - 1].stats + const latest: Record = {} + for (const [key, val] of Object.entries(last)) { + latest[key] = { avg: val.avg, loss: val.loss } + } + setLatestResults(latest) + } + }) + .catch(() => setStats([])) + + return () => controller.abort() + }, [systemId, chartTime, probes.length]) + + const deleteProbe = async (id: string) => { + try { + await pb.send("/api/beszel/network-probes", { + method: "DELETE", + query: { id }, + }) + fetchProbes() + } catch (err: any) { + toast({ variant: "destructive", title: t`Error`, description: err?.message }) + } + } + + const probeKey = (p: NetworkProbeRecord) => { + if (p.protocol === "tcp") return `${p.protocol}:${p.target}:${p.port}` + return `${p.protocol}:${p.target}` + } + + const dataPoints: DataPoint[] = useMemo(() => { + return probes.map((p, i) => { + const key = probeKey(p) + return { + label: p.name || p.target, + dataKey: (record: NetworkProbeStatsRecord) => record.stats[key]?.avg ?? null, + color: (i % 10) + 1, + } + }) + }, [probes]) + + if (probes.length === 0 && stats.length === 0) { + return ( + + +
+
+ + Network Probes + + + ICMP/TCP/HTTP latency monitoring from this agent + +
+ +
+
+
+ ) + } + + const protocolBadge = (protocol: string) => { + const colors: Record = { + icmp: "bg-blue-500/15 text-blue-400", + tcp: "bg-purple-500/15 text-purple-400", + http: "bg-green-500/15 text-green-400", + } + return ( + + {protocol} + + ) + } + + return ( +
+ + +
+
+ + Network Probes + + + ICMP/TCP/HTTP latency monitoring from this agent + +
+ +
+
+ +
+ + + + + + + + + + + + + + {probes.map((p) => { + const key = probeKey(p) + const result = latestResults[key] + return ( + + + + + + + + + + ) + })} + +
+ Name + + Target + + Protocol + + Interval + + Latency + + Loss +
{p.name || "-"}{p.target}{protocolBadge(p.protocol)}{p.interval}s + {result ? ( + 100 ? "text-yellow-400" : "text-green-400"}> + {toFixedFloat(result.avg, 1)} ms + + ) : ( + - + )} + + {result ? ( + 0 ? "text-red-400" : "text-green-400"}> + {toFixedFloat(result.loss, 1)}% + + ) : ( + - + )} + + +
+
+
+ + {stats.length > 0 && ( + + `${toFixedFloat(value, value >= 10 ? 0 : 1)} ms`} + contentFormatter={({ value }) => `${decimalString(value, 2)} ms`} + legend + /> + + )} +
+ ) +}