mirror of
https://github.com/henrygd/beszel.git
synced 2026-05-06 10:51:50 +02:00
238 lines
6.0 KiB
Go
238 lines
6.0 KiB
Go
import LineChartDefault from "@/components/charts/line-chart"
|
|
import type { DataPoint } from "@/components/charts/line-chart"
|
|
import { decimalString, formatMicroseconds, toFixedFloat } from "@/lib/utils"
|
|
import { useLingui } from "@lingui/react/macro"
|
|
import { ChartCard, FilterBar } from "../chart-card"
|
|
import type { ChartData, NetworkProbeRecord, NetworkProbeStatsRecord } from "@/types"
|
|
import { useMemo } from "react"
|
|
import { atom } from "nanostores"
|
|
import { useStore } from "@nanostores/react"
|
|
|
|
const $filter = atom("")
|
|
|
|
type ProbeChartProps = {
|
|
probeStats: NetworkProbeStatsRecord[]
|
|
grid?: boolean
|
|
probes: NetworkProbeRecord[]
|
|
chartData: ChartData
|
|
empty: boolean
|
|
showFilter?: boolean
|
|
}
|
|
|
|
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,
|
|
showFilter = probes.length > 1,
|
|
}: ProbeChartBaseProps) {
|
|
const storedFilter = useStore($filter)
|
|
const filter = showFilter ? storedFilter : ""
|
|
|
|
const { dataPoints, visibleKeys } = useMemo(() => {
|
|
const sortedProbes = [...probes].sort((a, b) => b.resAvg1h - a.resAvg1h)
|
|
const count = sortedProbes.length
|
|
const points: DataPoint<NetworkProbeStatsRecord>[] = []
|
|
const visibleIDs: string[] = []
|
|
const filterTerms = filter
|
|
? filter
|
|
.toLowerCase()
|
|
.split(" ")
|
|
.filter((term) => term.length > 0)
|
|
: []
|
|
const dot = chartData.chartTime === "1m"
|
|
for (let i = 0; i < count; i++) {
|
|
const p = sortedProbes[i]
|
|
const label = p.name || p.target
|
|
const filtered = filterTerms.length > 0 && !filterTerms.some((term) => label.toLowerCase().includes(term))
|
|
if (filtered) {
|
|
continue
|
|
}
|
|
visibleIDs.push(p.id)
|
|
points.push({
|
|
order: i,
|
|
label,
|
|
dataKey: (record: NetworkProbeStatsRecord) => record.stats?.[p.id]?.[valueIndex] ?? "-",
|
|
dot,
|
|
color: count <= 5 ? i + 1 : `hsl(${(i * 360) / count}, var(--chart-saturation), var(--chart-lightness))`,
|
|
})
|
|
}
|
|
return { dataPoints: points, visibleKeys: visibleIDs }
|
|
}, [probes, filter, valueIndex, chartData.chartTime])
|
|
|
|
const filteredProbeStats = useMemo(() => {
|
|
if (!visibleKeys.length) return probeStats
|
|
return probeStats.filter((record) => visibleKeys.some((id) => record.stats?.[id] != null))
|
|
}, [probeStats, visibleKeys])
|
|
|
|
const legend = dataPoints.length < 10 && dataPoints.length > 1
|
|
|
|
return (
|
|
<ChartCard
|
|
legend={legend || !showFilter}
|
|
cornerEl={showFilter ? <FilterBar store={$filter} /> : undefined}
|
|
empty={empty}
|
|
title={title}
|
|
description={description}
|
|
grid={grid}
|
|
>
|
|
<LineChartDefault
|
|
truncate
|
|
chartData={chartData}
|
|
customData={filteredProbeStats}
|
|
dataPoints={dataPoints}
|
|
domain={domain ?? ["auto", "auto"]}
|
|
connectNulls
|
|
tickFormatter={tickFormatter}
|
|
contentFormatter={contentFormatter}
|
|
legend={legend}
|
|
filter={filter}
|
|
/>
|
|
</ChartCard>
|
|
)
|
|
}
|
|
|
|
export function ResponseChart({ 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`Response`}
|
|
description={t`Average response time`}
|
|
tickFormatter={(value) => formatMicroseconds(value, false)}
|
|
contentFormatter={({ value }) => {
|
|
if (typeof value !== "number") {
|
|
return value
|
|
}
|
|
return formatMicroseconds(value)
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
interface AvgMinMaxResponseChartProps {
|
|
probeStats: NetworkProbeStatsRecord[]
|
|
probe: NetworkProbeRecord | null
|
|
chartData: ChartData
|
|
empty: boolean
|
|
}
|
|
|
|
export function AvgMinMaxResponseChart({ probeStats, probe, chartData, empty }: AvgMinMaxResponseChartProps) {
|
|
const { t } = useLingui()
|
|
|
|
const { chartTime } = chartData
|
|
const hasLongInterval = (probe?.interval ?? 61) > 60
|
|
|
|
// only one probe is relevant for this chart
|
|
const dataPoints: DataPoint<NetworkProbeStatsRecord>[] = useMemo(() => {
|
|
const dataFn = (index: number) => (record: NetworkProbeStatsRecord) =>
|
|
record.stats?.[probe?.id ?? ""]?.[index] ?? "-"
|
|
const avgPoint = {
|
|
label: "Avg",
|
|
dataKey: dataFn(0),
|
|
color: 1,
|
|
order: 0,
|
|
}
|
|
if (chartTime === "1m" || (hasLongInterval && chartTime === "1h")) {
|
|
// avg, min, max are all the same for 1m interval, so just show avg
|
|
return [avgPoint]
|
|
}
|
|
return [
|
|
{
|
|
label: "Max",
|
|
dataKey: dataFn(2),
|
|
color: 3,
|
|
order: 0,
|
|
},
|
|
avgPoint,
|
|
{
|
|
label: "Min",
|
|
dataKey: dataFn(1),
|
|
color: 2,
|
|
order: 2,
|
|
},
|
|
]
|
|
}, [chartTime, hasLongInterval])
|
|
|
|
const data = useMemo(() => {
|
|
if (!probe) return []
|
|
return probeStats.filter((record) => record.stats && probe.id in record.stats)
|
|
}, [probe, probeStats])
|
|
|
|
const legend = dataPoints.length > 1
|
|
|
|
return (
|
|
<ChartCard
|
|
legend={true}
|
|
empty={empty}
|
|
title={t`Response`}
|
|
description={t`Average, minimum, and maximum response time`}
|
|
grid={false}
|
|
>
|
|
<LineChartDefault
|
|
truncate
|
|
chartData={chartData}
|
|
customData={data}
|
|
dataPoints={dataPoints}
|
|
domain={["auto", "auto"]}
|
|
connectNulls
|
|
legend={legend}
|
|
tickFormatter={(value) => formatMicroseconds(value, false)}
|
|
contentFormatter={({ value }) => {
|
|
if (typeof value !== "number") {
|
|
return value
|
|
}
|
|
return formatMicroseconds(value)
|
|
}}
|
|
/>
|
|
</ChartCard>
|
|
)
|
|
}
|
|
|
|
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)}%`
|
|
}}
|
|
/>
|
|
)
|
|
}
|