mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 18:56:17 +01:00
improve bandwidth measurement precision + refactor default area chart
This commit is contained in:
@@ -1,128 +1,91 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils"
|
||||
// import Spinner from '../spinner'
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { useMemo } from "react"
|
||||
|
||||
/** [label, key, color, opacity] */
|
||||
type DataKeys = [string, string, number, number]
|
||||
|
||||
const getNestedValue = (path: string, max = false, data: any): number | null => {
|
||||
// fallback value (obj?.stats?.cpum ? 0 : null) should only come into play when viewing
|
||||
// a max value which doesn't exist, or the value was zero and omitted from the stats object.
|
||||
// so we check if cpum is present. if so, return 0 to make sure the zero value is displayed.
|
||||
// if not, return null - there is no max data so do not display anything.
|
||||
return `stats.${path}${max ? "m" : ""}`
|
||||
.split(".")
|
||||
.reduce((acc: any, key: string) => acc?.[key] ?? (data.stats?.cpum ? 0 : null), data)
|
||||
export type DataPoint = {
|
||||
label: string
|
||||
dataKey: (data: SystemStatsRecord) => number | undefined
|
||||
color: string
|
||||
opacity: number
|
||||
}
|
||||
|
||||
export default memo(function AreaChartDefault({
|
||||
maxToggled = false,
|
||||
chartName,
|
||||
export default function AreaChartDefault({
|
||||
chartData,
|
||||
max,
|
||||
maxToggled,
|
||||
tickFormatter,
|
||||
contentFormatter,
|
||||
}: {
|
||||
maxToggled?: boolean
|
||||
chartName: string
|
||||
dataPoints,
|
||||
}: // logRender = false,
|
||||
{
|
||||
chartData: ChartData
|
||||
max?: number
|
||||
tickFormatter: (value: number) => string
|
||||
contentFormatter: ({ value }: { value: number }) => string
|
||||
maxToggled?: boolean
|
||||
tickFormatter: (value: number, index: number) => string
|
||||
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
|
||||
dataPoints?: DataPoint[]
|
||||
// logRender?: boolean
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
const { i18n } = useLingui()
|
||||
|
||||
const { chartTime } = chartData
|
||||
|
||||
const showMax = chartTime !== "1h" && maxToggled
|
||||
|
||||
const dataKeys: DataKeys[] = useMemo(() => {
|
||||
// [label, key, color, opacity]
|
||||
if (chartName === "CPU Usage") {
|
||||
return [[t`CPU Usage`, "cpu", 1, 0.4]]
|
||||
} else if (chartName === "dio") {
|
||||
return [
|
||||
[t({ message: "Write", comment: "Disk write" }), "dw", 3, 0.3],
|
||||
[t({ message: "Read", comment: "Disk read" }), "dr", 1, 0.3],
|
||||
]
|
||||
} else if (chartName === "bw") {
|
||||
return [
|
||||
[t({ message: "Sent", comment: "Network bytes sent (upload)" }), "ns", 5, 0.2],
|
||||
[t({ message: "Received", comment: "Network bytes received (download)" }), "nr", 2, 0.2],
|
||||
]
|
||||
} else if (chartName.startsWith("efs")) {
|
||||
return [
|
||||
[t`Write`, `${chartName}.w`, 3, 0.3],
|
||||
[t`Read`, `${chartName}.r`, 1, 0.3],
|
||||
]
|
||||
} else if (chartName.startsWith("g.")) {
|
||||
return [chartName.includes("mu") ? [t`Used`, chartName, 2, 0.25] : [t`Usage`, chartName, 1, 0.4]]
|
||||
return useMemo(() => {
|
||||
if (chartData.systemStats.length === 0) {
|
||||
return null
|
||||
}
|
||||
return []
|
||||
}, [chartName, i18n.locale])
|
||||
|
||||
// console.log('Rendered at', new Date())
|
||||
|
||||
if (chartData.systemStats.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
domain={[0, max ?? "auto"]}
|
||||
tickFormatter={(value) => updateYAxisWidth(tickFormatter(value))}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={contentFormatter}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{dataKeys.map((key, i) => {
|
||||
const color = `hsl(var(--chart-${key[2]}))`
|
||||
return (
|
||||
<Area
|
||||
key={i}
|
||||
dataKey={getNestedValue.bind(null, key[1], showMax)}
|
||||
name={key[0]}
|
||||
type="monotoneX"
|
||||
fill={color}
|
||||
fillOpacity={key[3]}
|
||||
stroke={color}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)
|
||||
// if (logRender) {
|
||||
// console.log("Rendered at", new Date())
|
||||
// }
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
{/* <ChartLegend content={<ChartLegendContent />} /> */}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
>
|
||||
<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
domain={[0, max ?? "auto"]}
|
||||
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={contentFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{dataPoints?.map((dataPoint, i) => {
|
||||
const color = `hsl(var(--chart-${dataPoint.color}))`
|
||||
return (
|
||||
<Area
|
||||
key={i}
|
||||
dataKey={dataPoint.dataKey}
|
||||
name={dataPoint.label}
|
||||
type="monotoneX"
|
||||
fill={color}
|
||||
fillOpacity={dataPoint.opacity}
|
||||
stroke={color}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{/* <ChartLegend content={<ChartLegendContent />} /> */}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}, [chartData.systemStats.length, yAxisWidth, maxToggled])
|
||||
}
|
||||
|
||||
@@ -371,6 +371,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
|
||||
// select field for switching between avg and max values
|
||||
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
|
||||
const showMax = chartTime !== "1h" && maxValues
|
||||
|
||||
// if no data, show empty message
|
||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
|
||||
@@ -477,8 +478,15 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName="CPU Usage"
|
||||
maxToggled={maxValues}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`CPU Usage`,
|
||||
dataKey: ({ stats }) => (showMax ? stats?.cpum : stats?.cpu),
|
||||
color: "1",
|
||||
opacity: 0.4,
|
||||
},
|
||||
]}
|
||||
tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
|
||||
contentFormatter={({ value }) => decimalString(value) + "%"}
|
||||
/>
|
||||
@@ -530,8 +538,21 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName="dio"
|
||||
maxToggled={maxValues}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t({ message: "Write", comment: "Disk write" }),
|
||||
dataKey: ({ stats }) => (showMax ? stats?.dwm : stats?.dw),
|
||||
color: "3",
|
||||
opacity: 0.3,
|
||||
},
|
||||
{
|
||||
label: t({ message: "Read", comment: "Disk read" }),
|
||||
dataKey: ({ stats }) => (showMax ? stats?.drm : stats?.dr),
|
||||
color: "1",
|
||||
opacity: 0.3,
|
||||
},
|
||||
]}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
|
||||
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||
@@ -552,15 +573,39 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName="bw"
|
||||
maxToggled={maxValues}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`Sent`,
|
||||
// use bytes if available, otherwise multiply old MB (can remove in future)
|
||||
dataKey(data) {
|
||||
if (showMax) {
|
||||
return data?.stats?.bm?.[0] ?? (data?.stats?.nsm ?? 0) * 1024 * 1024
|
||||
}
|
||||
return data?.stats?.b?.[0] ?? data?.stats?.ns * 1024 * 1024
|
||||
},
|
||||
color: "5",
|
||||
opacity: 0.2,
|
||||
},
|
||||
{
|
||||
label: t`Received`,
|
||||
dataKey(data) {
|
||||
if (showMax) {
|
||||
return data?.stats?.bm?.[1] ?? (data?.stats?.nrm ?? 0) * 1024 * 1024
|
||||
}
|
||||
return data?.stats?.b?.[1] ?? data?.stats?.nr * 1024 * 1024
|
||||
},
|
||||
color: "2",
|
||||
opacity: 0.2,
|
||||
},
|
||||
]}
|
||||
tickFormatter={(val) => {
|
||||
let { value, unit } = formatBytes(val, true, userSettings.unitNet, true)
|
||||
let { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
|
||||
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, true)
|
||||
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||
contentFormatter={(data) => {
|
||||
const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)
|
||||
return decimalString(value, value >= 100 ? 1 : 2) + " " + unit
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
@@ -649,7 +694,14 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName={`g.${id}.u`}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`Usage`,
|
||||
dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,
|
||||
color: "1",
|
||||
opacity: 0.35,
|
||||
},
|
||||
]}
|
||||
tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
|
||||
contentFormatter={({ value }) => decimalString(value) + "%"}
|
||||
/>
|
||||
@@ -662,7 +714,14 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName={`g.${id}.mu`}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`Usage`,
|
||||
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
|
||||
color: "2",
|
||||
opacity: 0.25,
|
||||
},
|
||||
]}
|
||||
max={gpu.mt}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
|
||||
@@ -707,7 +766,20 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName={`efs.${extraFsName}`}
|
||||
dataPoints={[
|
||||
{
|
||||
label: t`Write`,
|
||||
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "wm" : "w"] ?? 0,
|
||||
color: "3",
|
||||
opacity: 0.3,
|
||||
},
|
||||
{
|
||||
label: t`Read`,
|
||||
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "rm" : "r"] ?? 0,
|
||||
color: "1",
|
||||
opacity: 0.3,
|
||||
},
|
||||
]}
|
||||
maxToggled={maxValues}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
|
||||
|
||||
@@ -260,19 +260,19 @@ export default function SystemsTable() {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.b || 0,
|
||||
accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024,
|
||||
id: "net",
|
||||
name: () => t`Net`,
|
||||
size: 0,
|
||||
Icon: EthernetIcon,
|
||||
header: sortableHeader,
|
||||
cell(info) {
|
||||
if (info.row.original.status === "paused") {
|
||||
const sys = info.row.original
|
||||
if (sys.status === "paused") {
|
||||
return null
|
||||
}
|
||||
const val = info.getValue() as number
|
||||
const userSettings = useStore($userSettings)
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitNet, true)
|
||||
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false)
|
||||
return (
|
||||
<span className="tabular-nums whitespace-nowrap">
|
||||
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
||||
|
||||
Reference in New Issue
Block a user