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 /> )}
) }