feat(site): compare network monitors across systems (#2442)

This commit is contained in:
Sven van Ginkel
2026-09-28 23:45:54 +02:00
committed by GitHub
parent 7626e5e3c8
commit 666529e073
6 changed files with 353 additions and 96 deletions

View File

@@ -194,18 +194,23 @@ export function formatBulkMonitorLine(monitor: BulkMonitorLineSource) {
return trimTrailingEmptyFields([monitor.target, monitor.protocol, port, interval, server]).join(",")
}
function SystemMultiSelect({
export function SystemMultiSelect({
id,
selectedSystemIds,
onChange,
disabled,
className,
systemIds,
placeholder,
}: {
id: string
selectedSystemIds: Set<string>
onChange: (ids: Set<string>) => void
disabled?: boolean
className?: string
/** Limit the options to these systems. Defaults to all systems that support network monitors. */
systemIds?: string[]
placeholder?: string
}) {
const systems = useStore($systems)
const { t } = useLingui()
@@ -221,7 +226,9 @@ function SystemMultiSelect({
const contentRef = useRef<HTMLDivElement>(null)
const query = search.trim().toLocaleLowerCase()
const filteredSystems = systems.filter(
(system) => supportsNetworkMonitors(system) && system.name.toLocaleLowerCase().includes(query)
(system) =>
(systemIds ? systemIds.includes(system.id) : supportsNetworkMonitors(system)) &&
system.name.toLocaleLowerCase().includes(query)
)
const allSelected = filteredSystems.every((system) => selectedSystemIds.has(system.id))
const anySelected = filteredSystems.some((system) => selectedSystemIds.has(system.id))
@@ -247,7 +254,7 @@ function SystemMultiSelect({
<ServerIcon className="size-3.5 absolute start-4 top-1/2 -translate-y-1/2 opacity-85" />
<span className="truncate">
{selectedSystemIds.size === 0
? t`Select systems`
? (placeholder ?? t`Select systems`)
: selectedSystemIds.size === 1
? systems.find((s) => selectedSystemIds.has(s.id))?.name
: t`${selectedSystemIds.size} selected`}

View File

@@ -39,7 +39,7 @@ import { SystemStatus } from "@/lib/enums"
import { $allSystemsById, $direction, $textMeasureVersion, $userSettings, getUserChartTime } from "@/lib/stores"
import { cn, formatShortDate, isVisuallyLonger, matchesFilterGroups, parseFilterGroups, parseSemVer } from "@/lib/utils"
import type { ChartData, MonitorCertInfo, NetworkMonitorRecord } from "@/types"
import { AddMonitorDialog, EditMonitorDialog } from "./monitor-dialog"
import { AddMonitorDialog, EditMonitorDialog, SystemMultiSelect } from "./monitor-dialog"
import {
ArrowDownIcon,
ArrowLeftRightIcon,
@@ -66,8 +66,8 @@ import {
} from "@/components/ui/dropdown-menu"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import ChartTimeSelect from "@/components/charts/chart-time-select"
import { LossChart, AvgMinMaxResponseChart } from "@/components/routes/system/charts/monitors-charts"
import { useNetworkMonitorStats } from "@/lib/use-network-monitors"
import { LossChart, AvgMinMaxResponseChart, ResponseChart } from "@/components/routes/system/charts/monitors-charts"
import { useMatchingMonitors, useNetworkMonitorStats } from "@/lib/use-network-monitors"
import { useStore } from "@nanostores/react"
import { atom } from "nanostores"
import { Separator } from "../ui/separator"
@@ -701,11 +701,33 @@ function NetworkMonitorSheetContent({
})
const chartTime = useStore(chartTimeStore)
const direction = useStore($direction)
const system = useStore($allSystemsById)[monitor.system]
const systems = useStore($allSystemsById)
const system = systems[monitor.system]
// Same target probed from other systems, for side-by-side comparison (#2385).
const matchingMonitors = useMatchingMonitors(monitor, open)
const [compareSystemIds, setCompareSystemIds] = useState<Set<string>>(() => new Set())
// Scoped to this sheet so a filter doesn't carry over to other monitors' sheets.
const [compareFilterStore, setCompareFilterStore] = useState(() => atom(""))
// The sheet is keyed by system (to keep the time range), so reset comparison state per monitor.
const [compareMonitorId, setCompareMonitorId] = useState(monitor.id)
if (compareMonitorId !== monitor.id) {
setCompareMonitorId(monitor.id)
setCompareSystemIds(new Set())
setCompareFilterStore(atom(""))
}
const matchingSystemIds = useMemo(() => matchingMonitors.map((m) => m.system), [matchingMonitors])
// The opened system is always charted; the picker only adds other systems to compare against.
const compareMonitors = useMemo(
() => [monitor, ...matchingMonitors.filter((m) => compareSystemIds.has(m.system))],
[monitor, matchingMonitors, compareSystemIds]
)
const comparing = compareMonitors.length > 1
const getSystemName = useCallback((m: NetworkMonitorRecord) => systems[m.system]?.name ?? m.system, [systems])
const monitorStats = useNetworkMonitorStats({
systemId: monitor.system,
monitorId: monitor.id,
monitorIds: comparing ? compareMonitors.map((m) => m.id) : [monitor.id],
interval: monitor.interval,
chartTime,
enabled: open,
@@ -719,7 +741,9 @@ function NetworkMonitorSheetContent({
}),
[system?.info?.v, direction, chartTime]
)
const hasMonitorStats = monitorStats.some((record) => record.stats?.[monitor.id] != null)
const hasMonitorStats = comparing
? monitorStats.some((record) => record.stats != null)
: monitorStats.some((record) => record.stats?.[monitor.id] != null)
const monitorLabel = getMonitorTarget(monitor)
return (
@@ -753,26 +777,63 @@ function NetworkMonitorSheetContent({
</SheetDescription>
</SheetHeader>
<div className="grid gap-4">
<ChartTimeSelect
className="bg-card"
agentVersion={chartData.agentVersion}
chartTimeStore={chartTimeStore}
allowRealtime={false}
/>
<AvgMinMaxResponseChart
monitorStats={monitorStats}
monitor={monitor}
chartData={chartData}
empty={!hasMonitorStats}
/>
<LossChart
monitorStats={monitorStats}
grid={false}
monitors={[monitor]}
chartData={chartData}
empty={!hasMonitorStats}
showFilter={false}
/>
<div className="flex flex-wrap items-center gap-2">
<ChartTimeSelect
className="bg-card flex-1 basis-48"
agentVersion={chartData.agentVersion}
chartTimeStore={chartTimeStore}
allowRealtime={false}
/>
{matchingMonitors.length > 0 && (
<SystemMultiSelect
id="monitor-compare-systems"
className="w-full sm:w-1/3 shrink-0 bg-card"
systemIds={matchingSystemIds}
selectedSystemIds={compareSystemIds}
onChange={setCompareSystemIds}
placeholder={t`Compare with other systems`}
/>
)}
</div>
{comparing ? (
<>
<ResponseChart
monitorStats={monitorStats}
grid={false}
monitors={compareMonitors}
chartData={chartData}
empty={!hasMonitorStats}
getLabel={getSystemName}
filterStore={compareFilterStore}
/>
<LossChart
monitorStats={monitorStats}
grid={false}
monitors={compareMonitors}
chartData={chartData}
empty={!hasMonitorStats}
getLabel={getSystemName}
filterStore={compareFilterStore}
/>
</>
) : (
<>
<AvgMinMaxResponseChart
monitorStats={monitorStats}
monitor={monitor}
chartData={chartData}
empty={!hasMonitorStats}
/>
<LossChart
monitorStats={monitorStats}
grid={false}
monitors={[monitor]}
chartData={chartData}
empty={!hasMonitorStats}
showFilter={false}
/>
</>
)}
</div>
</SheetContent>
</Sheet>