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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+