use network probes

This commit is contained in:
henrygd
2026-04-21 15:29:46 -04:00
parent a95376b4a2
commit 48fe407292
6 changed files with 326 additions and 191 deletions

View File

@@ -1,13 +1,13 @@
import { useLingui } from "@lingui/react/macro"
import { memo, useEffect, useState } from "react"
import { memo, useEffect } from "react"
import NetworkProbesTableNew from "@/components/network-probes-table/network-probes-table"
import { ActiveAlerts } from "@/components/active-alerts"
import { FooterRepoLink } from "@/components/footer-repo-link"
import type { NetworkProbeRecord } from "@/types"
import { useNetworkProbesData } from "@/lib/use-network-probes"
export default memo(() => {
const { t } = useLingui()
const [probes, setProbes] = useState<NetworkProbeRecord[]>([])
const { probes } = useNetworkProbesData({})
useEffect(() => {
document.title = `${t`Network Probes`} / Beszel`
@@ -17,7 +17,7 @@ export default memo(() => {
<>
<div className="grid gap-4">
<ActiveAlerts />
<NetworkProbesTableNew probes={probes} setProbes={setProbes} />
<NetworkProbesTableNew probes={probes} />
</div>
<FooterRepoLink />
</>

View File

@@ -28,7 +28,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
system,
systemStats,
containerData,
probeStats,
chartData,
containerChartConfigs,
details,
@@ -148,8 +147,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
{hasSystemd && <LazySystemdTable systemId={system.id} />}
<LazyNetworkProbesTable systemId={system.id} systemData={systemData} />
{/* <LazyNetworkProbesTable system={system} chartData={chartData} grid={grid} probeStats={probeStats} /> */}
</>
)
}
@@ -198,7 +195,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
{pageBottomExtraMargin > 0 && <div style={{ marginBottom: pageBottomExtraMargin }}></div>}
</div>
<LazyNetworkProbesTable systemId={system.id} systemData={systemData} />
{/* <LazyNetworkProbesTable system={system} chartData={chartData} grid={grid} probeStats={probeStats} /> */}
</TabsContent>
<TabsContent value="disk" forceMount className={activeTab === "disk" ? "contents" : "hidden"}>

View File

@@ -15,27 +15,43 @@ function probeKey(p: NetworkProbeRecord) {
const $filter = atom("")
export function LatencyChart({
probeStats,
grid,
probes,
chartData,
empty,
}: {
type ProbeChartProps = {
probeStats: NetworkProbeStatsRecord[]
grid?: boolean
probes: NetworkProbeRecord[]
chartData: ChartData
empty: boolean
}) {
const { t } = useLingui()
}
type ProbeChartBaseProps = ProbeChartProps & {
valueIndex: number
title: string
description: string
tickFormatter: (value: number) => string
contentFormatter: ({ value }: { value: number | string }) => string | number
domain?: [number | "auto", number | "auto"]
}
function ProbeChart({
probeStats,
grid,
probes,
chartData,
empty,
valueIndex,
title,
description,
tickFormatter,
contentFormatter,
domain,
}: ProbeChartBaseProps) {
const filter = useStore($filter)
const { dataPoints, visibleKeys } = useMemo(() => {
const count = probes.length
const sortedProbes = [...probes].sort((a, b) => a.name.localeCompare(b.name))
const count = sortedProbes.length
const points: DataPoint<NetworkProbeStatsRecord>[] = []
const visibleKeys: string[] = []
probes.sort((a, b) => a.name.localeCompare(b.name))
const filterTerms = filter
? filter
.toLowerCase()
@@ -43,7 +59,7 @@ export function LatencyChart({
.filter((term) => term.length > 0)
: []
for (let i = 0; i < count; i++) {
const p = probes[i]
const p = sortedProbes[i]
const key = probeKey(p)
const filtered = filterTerms.length > 0 && !filterTerms.some((term) => key.toLowerCase().includes(term))
if (filtered) {
@@ -52,12 +68,12 @@ export function LatencyChart({
visibleKeys.push(key)
points.push({
label: p.name || p.target,
dataKey: (record: NetworkProbeStatsRecord) => record.stats?.[key]?.[0] ?? "-",
dataKey: (record: NetworkProbeStatsRecord) => record.stats?.[key]?.[valueIndex] ?? "-",
color: count <= 5 ? i + 1 : `hsl(${(i * 360) / count}, var(--chart-saturation), var(--chart-lightness))`,
})
}
return { dataPoints: points, visibleKeys }
}, [probes, filter])
}, [probes, filter, valueIndex])
const filteredProbeStats = useMemo(() => {
if (!visibleKeys.length) return probeStats
@@ -71,26 +87,70 @@ export function LatencyChart({
legend={legend}
cornerEl={<FilterBar store={$filter} />}
empty={empty}
title={t`Latency`}
description={t`Average round-trip time (ms)`}
title={title}
description={description}
grid={grid}
>
<LineChartDefault
chartData={chartData}
customData={filteredProbeStats}
dataPoints={dataPoints}
domain={["auto", "auto"]}
domain={domain ?? ["auto", "auto"]}
connectNulls
tickFormatter={(value) => `${toFixedFloat(value, value >= 10 ? 0 : 1)} ms`}
contentFormatter={({ value }) => {
if (value === "-") {
return value
}
return `${decimalString(value, 2)} ms`
}}
tickFormatter={tickFormatter}
contentFormatter={contentFormatter}
legend={legend}
filter={filter}
/>
</ChartCard>
)
}
export function LatencyChart({ probeStats, grid, probes, chartData, empty }: ProbeChartProps) {
const { t } = useLingui()
return (
<ProbeChart
probeStats={probeStats}
grid={grid}
probes={probes}
chartData={chartData}
empty={empty}
valueIndex={0}
title={t`Latency`}
description={t`Average round-trip time (ms)`}
tickFormatter={(value) => `${toFixedFloat(value, value >= 10 ? 0 : 1)} ms`}
contentFormatter={({ value }) => {
if (typeof value !== "number") {
return value
}
return `${decimalString(value, 2)} ms`
}}
/>
)
}
export function LossChart({ probeStats, grid, probes, chartData, empty }: ProbeChartProps) {
const { t } = useLingui()
return (
<ProbeChart
probeStats={probeStats}
grid={grid}
probes={probes}
chartData={chartData}
empty={empty}
valueIndex={3}
title={t`Loss`}
description={t`Packet loss (%)`}
domain={[0, 100]}
tickFormatter={(value) => `${toFixedFloat(value, value >= 10 ? 0 : 1)}%`}
contentFormatter={({ value }) => {
if (typeof value !== "number") {
return value
}
return `${decimalString(value, 2)}%`
}}
/>
)
}

View File

@@ -1,13 +1,11 @@
import { lazy, useEffect, useRef, useState } from "react"
import { lazy } from "react"
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
import { chartTimeData, cn } from "@/lib/utils"
import { NetworkProbeRecord, NetworkProbeStatsRecord } from "@/types"
import { LatencyChart } from "./charts/probes-charts"
import { SystemData } from "./use-system-data"
import { cn } from "@/lib/utils"
import { LatencyChart, LossChart } from "./charts/probes-charts"
import type { SystemData } from "./use-system-data"
import { $chartTime } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import system from "../system"
import { getStats, appendData } from "./chart-data"
import { useNetworkProbesData } from "@/lib/use-network-probes"
const ContainersTable = lazy(() => import("../../containers-table/containers-table"))
@@ -44,75 +42,43 @@ export function LazySystemdTable({ systemId }: { systemId: string }) {
const NetworkProbesTable = lazy(() => import("@/components/network-probes-table/network-probes-table"))
const cache = new Map<string, any>()
export function LazyNetworkProbesTable({ systemId, systemData }: { systemId: string; systemData: SystemData }) {
const { grid, chartData } = systemData ?? {}
const [probes, setProbes] = useState<NetworkProbeRecord[]>([])
const chartTime = useStore($chartTime)
const [probeStats, setProbeStats] = useState<NetworkProbeStatsRecord[]>([])
const { isIntersecting, ref } = useIntersectionObserver()
const statsRequestId = useRef(0)
// get stats when system "changes." (Not just system to system,
// also when new info comes in via systemManager realtime connection, indicating an update)
useEffect(() => {
if (!systemId || !chartTime || chartTime === "1m") {
return
}
const { expectedInterval } = chartTimeData[chartTime]
const ss_cache_key = `${systemId}${chartTime}`
const requestId = ++statsRequestId.current
const cachedProbeStats = cache.get(ss_cache_key) as NetworkProbeStatsRecord[] | undefined
// Render from cache immediately if available
if (cachedProbeStats?.length) {
setProbeStats(cachedProbeStats)
// Skip the fetch if the latest cached point is recent enough that no new point is expected yet
const lastCreated = cachedProbeStats.at(-1)?.created as number | undefined
if (lastCreated && Date.now() - lastCreated < expectedInterval * 0.9) {
return
}
}
getStats<NetworkProbeStatsRecord>("network_probe_stats", systemId, chartTime, cachedProbeStats).then(
(probeStats) => {
// If another request has been made since this one, ignore the results
if (requestId !== statsRequestId.current) {
return
}
// make new system stats
let probeStatsData = (cache.get(ss_cache_key) || []) as NetworkProbeStatsRecord[]
if (probeStats.length) {
probeStatsData = appendData(probeStatsData, probeStats, expectedInterval, 100)
cache.set(ss_cache_key, probeStatsData)
}
setProbeStats(probeStatsData)
}
)
}, [system, chartTime, probes])
return (
<div ref={ref} className={cn(isIntersecting && "contents")}>
{isIntersecting && (
<>
<NetworkProbesTable systemId={systemId} probes={probes} setProbes={setProbes} />
{!!chartData && (
<LatencyChart
probeStats={probeStats}
grid={grid}
probes={probes}
chartData={chartData}
empty={!probeStats.length}
/>
)}
</>
)}
{isIntersecting && <ProbesTable systemId={systemId} systemData={systemData} />}
</div>
)
}
function ProbesTable({ systemId, systemData }: { systemId: string; systemData: SystemData }) {
const { grid, chartData } = systemData ?? {}
const chartTime = useStore($chartTime)
const { probes, probeStats } = useNetworkProbesData({ systemId, loadStats: !!chartData, chartTime })
return (
<>
<NetworkProbesTable systemId={systemId} probes={probes} />
{!!chartData && !!probes.length && (
<div className="grid xl:grid-cols-2 gap-4">
<LatencyChart
probeStats={probeStats}
grid={grid}
probes={probes}
chartData={chartData}
empty={!probeStats.length}
/>
<LossChart
probeStats={probeStats}
grid={grid}
probes={probes}
chartData={chartData}
empty={!probeStats.length}
/>
</div>
)}
</>
)
}