mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-22 12:41:49 +02:00
use network probes
This commit is contained in:
@@ -13,27 +13,23 @@ import {
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table"
|
||||
import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual"
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { memo, useMemo, useRef, useState } from "react"
|
||||
import { getProbeColumns } from "@/components/network-probes-table/network-probes-columns"
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||
import { isReadOnlyUser } from "@/lib/api"
|
||||
import { $allSystemsById } from "@/lib/stores"
|
||||
import { cn, getVisualStringWidth, useBrowserStorage } from "@/lib/utils"
|
||||
import type { NetworkProbeRecord } from "@/types"
|
||||
import { AddProbeDialog } from "./probe-dialog"
|
||||
|
||||
const NETWORK_PROBE_FIELDS = "id,name,system,target,protocol,port,interval,latency,loss,enabled,updated"
|
||||
|
||||
export default function NetworkProbesTableNew({
|
||||
systemId,
|
||||
probes,
|
||||
setProbes,
|
||||
}: {
|
||||
systemId?: string
|
||||
probes: NetworkProbeRecord[]
|
||||
setProbes: React.Dispatch<React.SetStateAction<NetworkProbeRecord[]>>
|
||||
}) {
|
||||
const [sorting, setSorting] = useBrowserStorage<SortingState>(
|
||||
`sort-np-${systemId ? 1 : 0}`,
|
||||
@@ -44,88 +40,6 @@ export default function NetworkProbesTableNew({
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||
const [globalFilter, setGlobalFilter] = useState("")
|
||||
|
||||
// clear old data when systemId changes
|
||||
useEffect(() => {
|
||||
return setProbes([])
|
||||
}, [systemId])
|
||||
|
||||
useEffect(() => {
|
||||
function fetchData(systemId?: string) {
|
||||
pb.collection<NetworkProbeRecord>("network_probes")
|
||||
.getList(0, 2000, {
|
||||
fields: NETWORK_PROBE_FIELDS,
|
||||
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
|
||||
})
|
||||
.then((res) => setProbes(res.items))
|
||||
}
|
||||
|
||||
// initial load
|
||||
fetchData(systemId)
|
||||
|
||||
// if no systemId, pull after every system update
|
||||
// if (!systemId) {
|
||||
// return $allSystemsById.listen((_value, _oldValue, systemId) => {
|
||||
// // exclude initial load of systems
|
||||
// if (Date.now() - loadTime > 500) {
|
||||
// fetchData(systemId)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// if systemId, fetch after the system is updated
|
||||
// return listenKeys($allSystemsById, [systemId], (_newSystems) => {
|
||||
// fetchData(systemId)
|
||||
// })
|
||||
}, [systemId])
|
||||
|
||||
// Subscribe to updates
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined
|
||||
const pbOptions = systemId
|
||||
? { fields: NETWORK_PROBE_FIELDS, filter: pb.filter("system = {:system}", { system: systemId }) }
|
||||
: { fields: NETWORK_PROBE_FIELDS }
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
unsubscribe = await pb.collection<NetworkProbeRecord>("network_probes").subscribe(
|
||||
"*",
|
||||
(event) => {
|
||||
const record = event.record
|
||||
setProbes((currentProbes) => {
|
||||
const probes = currentProbes ?? []
|
||||
const matchesSystemScope = !systemId || record.system === systemId
|
||||
|
||||
if (event.action === "delete") {
|
||||
return probes.filter((device) => device.id !== record.id)
|
||||
}
|
||||
|
||||
if (!matchesSystemScope) {
|
||||
// Record moved out of scope; ensure it disappears locally.
|
||||
return probes.filter((device) => device.id !== record.id)
|
||||
}
|
||||
|
||||
const existingIndex = probes.findIndex((device) => device.id === record.id)
|
||||
if (existingIndex === -1) {
|
||||
return [record, ...probes]
|
||||
}
|
||||
|
||||
const next = [...probes]
|
||||
next[existingIndex] = record
|
||||
return next
|
||||
})
|
||||
},
|
||||
pbOptions
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to subscribe to SMART device updates:", error)
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
unsubscribe?.()
|
||||
}
|
||||
}, [systemId])
|
||||
|
||||
const { longestName, longestTarget } = useMemo(() => {
|
||||
let longestName = 0
|
||||
let longestTarget = 0
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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)}%`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
199
internal/site/src/lib/use-network-probes.ts
Normal file
199
internal/site/src/lib/use-network-probes.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { chartTimeData } from "@/lib/utils"
|
||||
import type { ChartTimes, NetworkProbeRecord, NetworkProbeStatsRecord } from "@/types"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { getStats, appendData } from "@/components/routes/system/chart-data"
|
||||
import { pb } from "@/lib/api"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import type { RecordListOptions, RecordSubscription } from "pocketbase"
|
||||
|
||||
const cache = new Map<string, NetworkProbeStatsRecord[]>()
|
||||
|
||||
const NETWORK_PROBE_FIELDS = "id,name,system,target,protocol,port,interval,latency,loss,enabled,updated"
|
||||
|
||||
interface UseNetworkProbesProps {
|
||||
systemId?: string
|
||||
loadStats?: boolean
|
||||
chartTime?: ChartTimes
|
||||
existingProbes?: NetworkProbeRecord[]
|
||||
}
|
||||
|
||||
export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
||||
const { systemId, loadStats, chartTime, existingProbes } = props
|
||||
|
||||
const [p, setProbes] = useState<NetworkProbeRecord[]>([])
|
||||
const [probeStats, setProbeStats] = useState<NetworkProbeStatsRecord[]>([])
|
||||
const statsRequestId = useRef(0)
|
||||
const pendingProbeEvents = useRef(new Map<string, RecordSubscription<NetworkProbeRecord>>())
|
||||
const probeBatchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const probes = existingProbes ?? p
|
||||
|
||||
// clear old data when systemId changes
|
||||
// useEffect(() => {
|
||||
// return setProbes([])
|
||||
// }, [systemId])
|
||||
|
||||
// initial load - fetch probes if not provided by caller
|
||||
useEffect(() => {
|
||||
if (!existingProbes) {
|
||||
fetchProbes(systemId).then((probes) => setProbes(probes))
|
||||
}
|
||||
}, [systemId])
|
||||
|
||||
// Subscribe to updates if probes not provided by caller
|
||||
useEffect(() => {
|
||||
if (existingProbes) {
|
||||
return
|
||||
}
|
||||
let unsubscribe: (() => void) | undefined
|
||||
|
||||
function flushPendingProbeEvents() {
|
||||
probeBatchTimeout.current = null
|
||||
if (!pendingProbeEvents.current.size) {
|
||||
return
|
||||
}
|
||||
const events = pendingProbeEvents.current
|
||||
pendingProbeEvents.current = new Map()
|
||||
setProbes((currentProbes) => {
|
||||
return applyProbeEvents(currentProbes ?? [], events.values(), systemId)
|
||||
})
|
||||
}
|
||||
|
||||
const pbOptions: RecordListOptions = { fields: NETWORK_PROBE_FIELDS }
|
||||
if (systemId) {
|
||||
pbOptions.filter = pb.filter("system = {:system}", { system: systemId })
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
unsubscribe = await pb.collection<NetworkProbeRecord>("network_probes").subscribe(
|
||||
"*",
|
||||
(event) => {
|
||||
pendingProbeEvents.current.set(event.record.id, event)
|
||||
if (!probeBatchTimeout.current) {
|
||||
probeBatchTimeout.current = setTimeout(flushPendingProbeEvents, 50)
|
||||
}
|
||||
},
|
||||
pbOptions
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to subscribe to probes", error)
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
if (probeBatchTimeout.current !== null) {
|
||||
clearTimeout(probeBatchTimeout.current)
|
||||
probeBatchTimeout.current = null
|
||||
}
|
||||
pendingProbeEvents.current.clear()
|
||||
unsubscribe?.()
|
||||
}
|
||||
}, [systemId])
|
||||
|
||||
// fetch probe stats when probes update
|
||||
useEffect(() => {
|
||||
if (!loadStats || !systemId || !chartTime || chartTime === "1m") {
|
||||
return
|
||||
}
|
||||
|
||||
const { expectedInterval } = chartTimeData[chartTime]
|
||||
const cache_key = `${systemId}${chartTime}`
|
||||
const requestId = ++statsRequestId.current
|
||||
|
||||
const cachedProbeStats = cache.get(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
|
||||
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(cache_key) || []) as NetworkProbeStatsRecord[]
|
||||
if (probeStats.length) {
|
||||
probeStatsData = appendData(probeStatsData, probeStats, expectedInterval, 100)
|
||||
cache.set(cache_key, probeStatsData)
|
||||
}
|
||||
setProbeStats(probeStatsData)
|
||||
}
|
||||
)
|
||||
}, [chartTime, probes])
|
||||
|
||||
return {
|
||||
probes,
|
||||
probeStats,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProbes(systemId?: string) {
|
||||
try {
|
||||
const res = await pb.collection<NetworkProbeRecord>("network_probes").getList(0, 2000, {
|
||||
fields: NETWORK_PROBE_FIELDS,
|
||||
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
|
||||
})
|
||||
return res.items
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: (error as Error)?.message,
|
||||
variant: "destructive",
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function applyProbeEvents(
|
||||
probes: NetworkProbeRecord[],
|
||||
events: Iterable<RecordSubscription<NetworkProbeRecord>>,
|
||||
systemId?: string
|
||||
) {
|
||||
// Use a map to handle updates/deletes in constant time
|
||||
const probeById = new Map(probes.map((probe) => [probe.id, probe]))
|
||||
const createdProbes: NetworkProbeRecord[] = []
|
||||
|
||||
for (const { action, record } of events) {
|
||||
const matchesSystemScope = !systemId || record.system === systemId
|
||||
|
||||
if (action === "delete" || !matchesSystemScope) {
|
||||
probeById.delete(record.id)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!probeById.has(record.id)) {
|
||||
createdProbes.push(record)
|
||||
}
|
||||
|
||||
probeById.set(record.id, record)
|
||||
}
|
||||
|
||||
const nextProbes: NetworkProbeRecord[] = []
|
||||
// Prepend brand new probes (matching previous behavior)
|
||||
for (let index = createdProbes.length - 1; index >= 0; index -= 1) {
|
||||
nextProbes.push(createdProbes[index])
|
||||
}
|
||||
|
||||
// Rebuild the final list while preserving original order for existing probes
|
||||
for (const probe of probes) {
|
||||
const nextProbe = probeById.get(probe.id)
|
||||
if (!nextProbe) {
|
||||
continue
|
||||
}
|
||||
nextProbes.push(nextProbe)
|
||||
probeById.delete(probe.id)
|
||||
}
|
||||
|
||||
return nextProbes
|
||||
}
|
||||
Reference in New Issue
Block a user