From 6576141f549e1e49350355a41818d8e4e89604e3 Mon Sep 17 00:00:00 2001 From: Anish Chanda Date: Mon, 14 Jul 2025 13:46:13 -0500 Subject: [PATCH] Adds display unit preference (#938) * Adds temperature unit preference * add unit preferences for networking * adds options for MB/s and bps. * supports disk throughput unit preferences --- beszel/internal/users/users.go | 6 + .../site/src/components/charts/area-chart.tsx | 35 +++++- .../src/components/charts/container-chart.tsx | 17 ++- .../components/charts/temperature-chart.tsx | 22 +++- .../components/routes/settings/general.tsx | 57 ++++++++++ .../systems-table/systems-table.tsx | 22 +++- beszel/site/src/lib/stores.ts | 3 + beszel/site/src/lib/utils.ts | 105 +++++++++++++++++- beszel/site/src/types.d.ts | 19 ++++ 9 files changed, 268 insertions(+), 18 deletions(-) diff --git a/beszel/internal/users/users.go b/beszel/internal/users/users.go index e200664..d14e94d 100644 --- a/beszel/internal/users/users.go +++ b/beszel/internal/users/users.go @@ -17,6 +17,9 @@ type UserSettings struct { ChartTime string `json:"chartTime"` NotificationEmails []string `json:"emails"` NotificationWebhooks []string `json:"webhooks"` + TemperatureUnit string `json:"temperatureUnit"` // "celsius" or "fahrenheit" + NetworkUnit string `json:"networkUnit"` // "mbps" (MB/s) or "bps" + DiskUnit string `json:"diskUnit"` // "mbps" (MB/s) or "bps" // Language string `json:"lang"` } @@ -43,6 +46,9 @@ func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error { ChartTime: "1h", NotificationEmails: []string{}, NotificationWebhooks: []string{}, + TemperatureUnit: "celsius", + NetworkUnit: "mbps", + DiskUnit: "mbps", } record.UnmarshalJSONField("settings", &settings) if len(settings.NotificationEmails) == 0 { diff --git a/beszel/site/src/components/charts/area-chart.tsx b/beszel/site/src/components/charts/area-chart.tsx index 8bdeeca..d3b62e3 100644 --- a/beszel/site/src/components/charts/area-chart.tsx +++ b/beszel/site/src/components/charts/area-chart.tsx @@ -9,11 +9,15 @@ import { toFixedWithoutTrailingZeros, decimalString, chartMargin, + convertNetworkSpeed, + convertDiskSpeed, } from "@/lib/utils" // import Spinner from '../spinner' import { ChartData } from "@/types" import { memo, useMemo } from "react" import { useLingui } from "@lingui/react/macro" +import { useStore } from "@nanostores/react" +import { $userSettings } from "@/lib/stores" /** [label, key, color, opacity] */ type DataKeys = [string, string, number, number] @@ -47,11 +51,26 @@ export default memo(function AreaChartDefault({ }) { const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { i18n } = useLingui() + const userSettings = useStore($userSettings) const { chartTime } = chartData const showMax = chartTime !== "1h" && maxToggled + // Determine if this is a network chart or disk chart and adjust unit accordingly + const isNetworkChart = chartName === "bw" + const isDiskChart = chartName === "dio" || chartName.startsWith("efs") + const displayUnit = useMemo(() => { + if (isNetworkChart) { + const { symbol } = convertNetworkSpeed(1, userSettings.networkUnit) + return symbol + } else if (isDiskChart) { + const { symbol } = convertDiskSpeed(1, userSettings.diskUnit) + return symbol + } + return unit + }, [isNetworkChart, isDiskChart, userSettings.networkUnit, userSettings.diskUnit, unit]) + const dataKeys: DataKeys[] = useMemo(() => { // [label, key, color, opacity] if (chartName === "CPU Usage") { @@ -102,8 +121,14 @@ export default memo(function AreaChartDefault({ let val: string if (tickFormatter) { val = tickFormatter(value) + } else if (isNetworkChart) { + const { value: convertedValue, symbol } = convertNetworkSpeed(value, userSettings.networkUnit) + val = toFixedWithoutTrailingZeros(convertedValue, 2) + symbol + } else if (isDiskChart) { + const { value: convertedValue, symbol } = convertDiskSpeed(value, userSettings.diskUnit) + val = toFixedWithoutTrailingZeros(convertedValue, 2) + symbol } else { - val = toFixedWithoutTrailingZeros(value, 2) + unit + val = toFixedWithoutTrailingZeros(value, 2) + displayUnit } return updateYAxisWidth(val) }} @@ -120,8 +145,14 @@ export default memo(function AreaChartDefault({ contentFormatter={({ value }) => { if (contentFormatter) { return contentFormatter(value) + } else if (isNetworkChart) { + const { display } = convertNetworkSpeed(value, userSettings.networkUnit) + return display + } else if (isDiskChart) { + const { display } = convertDiskSpeed(value, userSettings.diskUnit) + return display } - return decimalString(value) + unit + return decimalString(value) + displayUnit }} // indicator="line" /> diff --git a/beszel/site/src/components/charts/container-chart.tsx b/beszel/site/src/components/charts/container-chart.tsx index 05628bd..8a1ccd4 100644 --- a/beszel/site/src/components/charts/container-chart.tsx +++ b/beszel/site/src/components/charts/container-chart.tsx @@ -10,10 +10,11 @@ import { toFixedFloat, getSizeAndUnit, toFixedWithoutTrailingZeros, + convertNetworkSpeed, } from "@/lib/utils" // import Spinner from '../spinner' import { useStore } from "@nanostores/react" -import { $containerFilter } from "@/lib/stores" +import { $containerFilter, $userSettings } from "@/lib/stores" import { ChartData } from "@/types" import { Separator } from "../ui/separator" import { ChartType } from "@/lib/enums" @@ -30,6 +31,7 @@ export default memo(function ContainerChart({ unit?: string }) { const filter = useStore($containerFilter) + const userSettings = useStore($userSettings) const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { containerData } = chartData @@ -87,10 +89,15 @@ export default memo(function ContainerChart({ const val = toFixedWithoutTrailingZeros(value, 2) + unit return updateYAxisWidth(val) } + } else if (isNetChart) { + obj.tickFormatter = (value) => { + const { value: convertedValue, symbol } = convertNetworkSpeed(value, userSettings.networkUnit) + return updateYAxisWidth(`${toFixedFloat(convertedValue, 2)}${symbol}`) + } } else { obj.tickFormatter = (value) => { const { v, u } = getSizeAndUnit(value, false) - return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isNetChart ? "/s" : ""}`) + return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}`) } } // tooltip formatter @@ -99,12 +106,14 @@ export default memo(function ContainerChart({ try { const sent = item?.payload?.[key]?.ns ?? 0 const received = item?.payload?.[key]?.nr ?? 0 + const { display: receivedDisplay } = convertNetworkSpeed(received, userSettings.networkUnit) + const { display: sentDisplay } = convertNetworkSpeed(sent, userSettings.networkUnit) return ( - {decimalString(received)} MB/s + {receivedDisplay} rx - {decimalString(sent)} MB/s + {sentDisplay} tx ) diff --git a/beszel/site/src/components/charts/temperature-chart.tsx b/beszel/site/src/components/charts/temperature-chart.tsx index df672eb..ce85f47 100644 --- a/beszel/site/src/components/charts/temperature-chart.tsx +++ b/beszel/site/src/components/charts/temperature-chart.tsx @@ -15,14 +15,16 @@ import { toFixedWithoutTrailingZeros, decimalString, chartMargin, + convertTemperature, } from "@/lib/utils" import { ChartData } from "@/types" import { memo, useMemo } from "react" -import { $temperatureFilter } from "@/lib/stores" +import { $temperatureFilter, $userSettings } from "@/lib/stores" import { useStore } from "@nanostores/react" export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) { const filter = useStore($temperatureFilter) + const userSettings = useStore($userSettings) const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() if (chartData.systemStats.length === 0) { @@ -36,13 +38,17 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD colors: Record } const tempSums = {} as Record + const unit = userSettings.temperatureUnit || "celsius" + for (let data of chartData.systemStats) { let newData = { created: data.created } as Record let keys = Object.keys(data.stats?.t ?? {}) for (let i = 0; i < keys.length; i++) { let key = keys[i] - newData[key] = data.stats.t![key] - tempSums[key] = (tempSums[key] ?? 0) + newData[key] + const celsiusTemp = data.stats.t![key] + const { value } = convertTemperature(celsiusTemp, unit) + newData[key] = value + tempSums[key] = (tempSums[key] ?? 0) + value } newChartData.data.push(newData) } @@ -51,7 +57,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)` } return newChartData - }, [chartData]) + }, [chartData, userSettings.temperatureUnit]) const colors = Object.keys(newChartData.colors) @@ -74,7 +80,8 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD width={yAxisWidth} tickFormatter={(value) => { const val = toFixedWithoutTrailingZeros(value, 2) - return updateYAxisWidth(val + " °C") + const { symbol } = convertTemperature(0, userSettings.temperatureUnit || "celsius") + return updateYAxisWidth(val + " " + symbol) }} tickLine={false} axisLine={false} @@ -88,7 +95,10 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD content={ formatShortDate(data[0].payload.created)} - contentFormatter={(item) => decimalString(item.value) + " °C"} + contentFormatter={(item) => { + const { symbol } = convertTemperature(0, userSettings.temperatureUnit || "celsius") + return decimalString(item.value) + " " + symbol + }} filter={filter} /> } diff --git a/beszel/site/src/components/routes/settings/general.tsx b/beszel/site/src/components/routes/settings/general.tsx index 12c2076..0c8aa94 100644 --- a/beszel/site/src/components/routes/settings/general.tsx +++ b/beszel/site/src/components/routes/settings/general.tsx @@ -101,6 +101,63 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us

+
+
+

+ Unit preferences +

+

+ Adjust Display units for metrics. +

+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+