diff --git a/beszel/internal/users/users.go b/beszel/internal/users/users.go index d14e94d..e994f19 100644 --- a/beszel/internal/users/users.go +++ b/beszel/internal/users/users.go @@ -17,10 +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"` + // UnitTemp uint8 `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit + // UnitNet uint8 `json:"unitNet"` // 0 for bytes, 1 for bits + // UnitDisk uint8 `json:"unitDisk"` // 0 for bytes, 1 for bits } func NewUserManager(app core.App) *UserManager { @@ -42,13 +41,9 @@ func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error { record := e.Record // intialize settings with defaults settings := UserSettings{ - // Language: "en", 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 d3b62e3..814ad28 100644 --- a/beszel/site/src/components/charts/area-chart.tsx +++ b/beszel/site/src/components/charts/area-chart.tsx @@ -2,22 +2,11 @@ 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, - toFixedWithoutTrailingZeros, - decimalString, - chartMargin, - convertNetworkSpeed, - convertDiskSpeed, -} from "@/lib/utils" +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 { useStore } from "@nanostores/react" -import { $userSettings } from "@/lib/stores" /** [label, key, color, opacity] */ type DataKeys = [string, string, number, number] @@ -34,7 +23,6 @@ const getNestedValue = (path: string, max = false, data: any): number | null => export default memo(function AreaChartDefault({ maxToggled = false, - unit = " MB/s", chartName, chartData, max, @@ -42,35 +30,19 @@ export default memo(function AreaChartDefault({ contentFormatter, }: { maxToggled?: boolean - unit?: string chartName: string chartData: ChartData max?: number - tickFormatter?: (value: number) => string - contentFormatter?: (value: number) => string + tickFormatter: (value: number) => string + contentFormatter: ({ value }: { value: number }) => string }) { 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") { @@ -117,21 +89,7 @@ export default memo(function AreaChartDefault({ className="tracking-tighter" width={yAxisWidth} domain={[0, max ?? "auto"]} - tickFormatter={(value) => { - 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) + displayUnit - } - return updateYAxisWidth(val) - }} + tickFormatter={(value) => updateYAxisWidth(tickFormatter(value))} tickLine={false} axisLine={false} /> @@ -142,18 +100,7 @@ export default memo(function AreaChartDefault({ content={ formatShortDate(data[0].payload.created)} - 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) + displayUnit - }} + contentFormatter={contentFormatter} // indicator="line" /> } diff --git a/beszel/site/src/components/charts/container-chart.tsx b/beszel/site/src/components/charts/container-chart.tsx index 8a1ccd4..f008b1a 100644 --- a/beszel/site/src/components/charts/container-chart.tsx +++ b/beszel/site/src/components/charts/container-chart.tsx @@ -5,19 +5,17 @@ import { useYAxisWidth, cn, formatShortDate, - decimalString, chartMargin, - toFixedFloat, - getSizeAndUnit, toFixedWithoutTrailingZeros, - convertNetworkSpeed, + formatBytes, + decimalString, } from "@/lib/utils" // import Spinner from '../spinner' import { useStore } from "@nanostores/react" import { $containerFilter, $userSettings } from "@/lib/stores" import { ChartData } from "@/types" import { Separator } from "../ui/separator" -import { ChartType } from "@/lib/enums" +import { ChartType, Unit } from "@/lib/enums" export default memo(function ContainerChart({ dataKey, @@ -31,7 +29,7 @@ export default memo(function ContainerChart({ unit?: string }) { const filter = useStore($containerFilter) - const userSettings = useStore($userSettings) + const userSettings = $userSettings.get() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { containerData } = chartData @@ -89,15 +87,11 @@ 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}`) + const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes + obj.tickFormatter = (val) => { + const { value, unit } = formatBytes(val, isNetChart, chartUnit, true) + return updateYAxisWidth(decimalString(value, value >= 10 ? 0 : 1) + " " + unit) } } // tooltip formatter @@ -106,14 +100,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) + const { value: receivedValue, unit: receivedUnit } = formatBytes(received, true, userSettings.unitNet, true) + const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, true) return ( - {receivedDisplay} + {decimalString(receivedValue)} {receivedUnit} rx - {sentDisplay} + {decimalString(sentValue)} {sentUnit} tx ) @@ -123,8 +117,8 @@ export default memo(function ContainerChart({ } } else if (chartType === ChartType.Memory) { obj.toolTipFormatter = (item: any) => { - const { v, u } = getSizeAndUnit(item.value, false) - return decimalString(v, 2) + u + const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true) + return decimalString(value) + " " + unit } } else { obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit diff --git a/beszel/site/src/components/charts/disk-chart.tsx b/beszel/site/src/components/charts/disk-chart.tsx index 1db5f72..fceedb8 100644 --- a/beszel/site/src/components/charts/disk-chart.tsx +++ b/beszel/site/src/components/charts/disk-chart.tsx @@ -1,17 +1,10 @@ import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" -import { - useYAxisWidth, - cn, - formatShortDate, - decimalString, - toFixedFloat, - chartMargin, - getSizeAndUnit, -} from "@/lib/utils" +import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes } from "@/lib/utils" import { ChartData } from "@/types" import { memo } from "react" import { useLingui } from "@lingui/react/macro" +import { Unit } from "@/lib/enums" export default memo(function DiskChart({ dataKey, @@ -53,9 +46,9 @@ export default memo(function DiskChart({ minTickGap={6} tickLine={false} axisLine={false} - tickFormatter={(value) => { - const { v, u } = getSizeAndUnit(value) - return updateYAxisWidth(toFixedFloat(v, 2) + u) + tickFormatter={(val) => { + const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true) + return updateYAxisWidth(decimalString(value, value >= 10 ? 0 : 1) + " " + unit) }} /> {xAxis(chartData)} @@ -66,8 +59,8 @@ export default memo(function DiskChart({ formatShortDate(data[0].payload.created)} contentFormatter={({ value }) => { - const { v, u } = getSizeAndUnit(value) - return decimalString(v) + u + const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true) + return decimalString(convertedValue) + " " + unit }} /> } diff --git a/beszel/site/src/components/charts/mem-chart.tsx b/beszel/site/src/components/charts/mem-chart.tsx index b352e93..f87f43c 100644 --- a/beszel/site/src/components/charts/mem-chart.tsx +++ b/beszel/site/src/components/charts/mem-chart.tsx @@ -1,9 +1,10 @@ import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" -import { useYAxisWidth, cn, toFixedFloat, decimalString, formatShortDate, chartMargin } from "@/lib/utils" +import { useYAxisWidth, cn, toFixedFloat, decimalString, formatShortDate, chartMargin, formatBytes } from "@/lib/utils" import { memo } from "react" import { ChartData } from "@/types" import { useLingui } from "@lingui/react/macro" +import { Unit } from "@/lib/enums" export default memo(function MemChart({ chartData }: { chartData: ChartData }) { const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() @@ -39,8 +40,8 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) { tickLine={false} axisLine={false} tickFormatter={(value) => { - const val = toFixedFloat(value, 1) - return updateYAxisWidth(val + " GB") + const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true) + return updateYAxisWidth(decimalString(convertedValue, value >= 10 ? 0 : 1) + " " + unit) }} /> )} @@ -54,8 +55,11 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) { // @ts-ignore itemSorter={(a, b) => a.order - b.order} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} - contentFormatter={(item) => decimalString(item.value) + " GB"} - // indicator="line" + contentFormatter={({ value }) => { + // mem values are supplied as GB + const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true) + return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit + }} /> } /> diff --git a/beszel/site/src/components/charts/swap-chart.tsx b/beszel/site/src/components/charts/swap-chart.tsx index 09216e1..3ba325b 100644 --- a/beszel/site/src/components/charts/swap-chart.tsx +++ b/beszel/site/src/components/charts/swap-chart.tsx @@ -1,4 +1,4 @@ -import { t } from "@lingui/core/macro"; +import { t } from "@lingui/core/macro" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" @@ -9,12 +9,15 @@ import { toFixedWithoutTrailingZeros, decimalString, chartMargin, + formatBytes, } from "@/lib/utils" import { ChartData } from "@/types" import { memo } from "react" +import { $userSettings } from "@/lib/stores" export default memo(function SwapChart({ chartData }: { chartData: ChartData }) { const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() + const userSettings = $userSettings.get() if (chartData.systemStats.length === 0) { return null @@ -37,7 +40,10 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData }) width={yAxisWidth} tickLine={false} axisLine={false} - tickFormatter={(value) => updateYAxisWidth(value + " GB")} + tickFormatter={(value) => { + const { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true) + return updateYAxisWidth(decimalString(convertedValue, value >= 10 ? 0 : 1) + " " + unit) + }} /> {xAxis(chartData)} formatShortDate(data[0].payload.created)} - contentFormatter={(item) => decimalString(item.value) + " GB"} + contentFormatter={({ value }) => { + // mem values are supplied as GB + const { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true) + return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit + }} // indicator="line" /> } diff --git a/beszel/site/src/components/charts/temperature-chart.tsx b/beszel/site/src/components/charts/temperature-chart.tsx index ce85f47..8a87298 100644 --- a/beszel/site/src/components/charts/temperature-chart.tsx +++ b/beszel/site/src/components/charts/temperature-chart.tsx @@ -13,9 +13,9 @@ import { cn, formatShortDate, toFixedWithoutTrailingZeros, - decimalString, chartMargin, - convertTemperature, + formatTemperature, + decimalString, } from "@/lib/utils" import { ChartData } from "@/types" import { memo, useMemo } from "react" @@ -38,17 +38,13 @@ 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] - const celsiusTemp = data.stats.t![key] - const { value } = convertTemperature(celsiusTemp, unit) - newData[key] = value - tempSums[key] = (tempSums[key] ?? 0) + value + newData[key] = data.stats.t![key] + tempSums[key] = (tempSums[key] ?? 0) + newData[key] } newChartData.data.push(newData) } @@ -57,7 +53,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, userSettings.temperatureUnit]) + }, [chartData]) const colors = Object.keys(newChartData.colors) @@ -78,10 +74,9 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD className="tracking-tighter" domain={[0, "auto"]} width={yAxisWidth} - tickFormatter={(value) => { - const val = toFixedWithoutTrailingZeros(value, 2) - const { symbol } = convertTemperature(0, userSettings.temperatureUnit || "celsius") - return updateYAxisWidth(val + " " + symbol) + tickFormatter={(val) => { + const { value, unit } = formatTemperature(val, userSettings.unitTemp) + return updateYAxisWidth(toFixedWithoutTrailingZeros(value, 2) + " " + unit) }} tickLine={false} axisLine={false} @@ -96,8 +91,8 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD formatShortDate(data[0].payload.created)} contentFormatter={(item) => { - const { symbol } = convertTemperature(0, userSettings.temperatureUnit || "celsius") - return decimalString(item.value) + " " + symbol + const { value, unit } = formatTemperature(item.value, userSettings.unitTemp) + return decimalString(value) + " " + unit }} filter={filter} /> diff --git a/beszel/site/src/components/routes/settings/general.tsx b/beszel/site/src/components/routes/settings/general.tsx index 0c8aa94..c2cfb55 100644 --- a/beszel/site/src/components/routes/settings/general.tsx +++ b/beszel/site/src/components/routes/settings/general.tsx @@ -11,7 +11,7 @@ import { useState } from "react" import languages from "@/lib/languages" import { dynamicActivate } from "@/lib/i18n" import { useLingui } from "@lingui/react/macro" -// import { setLang } from "@/lib/i18n" +import { Unit } from "@/lib/enums" export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) { const [isLoading, setIsLoading] = useState(false) @@ -107,51 +107,75 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us Unit preferences

- Adjust Display units for metrics. + Change display units for metrics.

-
+
-
-
-
diff --git a/beszel/site/src/components/routes/system.tsx b/beszel/site/src/components/routes/system.tsx index 14540cf..aa3cad4 100644 --- a/beszel/site/src/components/routes/system.tsx +++ b/beszel/site/src/components/routes/system.tsx @@ -11,7 +11,7 @@ import { $temperatureFilter, } from "@/lib/stores" import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types" -import { ChartType, Os } from "@/lib/enums" +import { ChartType, Unit, Os } from "@/lib/enums" import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card" import { useStore } from "@nanostores/react" @@ -21,11 +21,12 @@ import ChartTimeSelect from "../charts/chart-time-select" import { chartTimeData, cn, + decimalString, + formatBytes, getHostDisplayValue, getPbTimestamp, - getSizeAndUnit, listen, - toFixedFloat, + toFixedWithoutTrailingZeros, useLocalStorage, } from "@/lib/utils" import { Separator } from "../ui/separator" @@ -131,6 +132,7 @@ export default function SystemDetail({ name }: { name: string }) { const [bottomSpacing, setBottomSpacing] = useState(0) const [chartLoading, setChartLoading] = useState(true) const isLongerChart = chartTime !== "1h" + const userSettings = $userSettings.get() useEffect(() => { document.title = `${name} / Beszel` @@ -472,7 +474,13 @@ export default function SystemDetail({ name }: { name: string }) { description={t`Average system-wide CPU utilization`} cornerEl={maxValSelect} > - + toFixedWithoutTrailingZeros(val, 2) + "%"} + contentFormatter={({ value }) => decimalString(value) + "%"} + /> {containerFilterBar && ( @@ -519,7 +527,19 @@ export default function SystemDetail({ name }: { name: string }) { description={t`Throughput of root filesystem`} cornerEl={maxValSelect} > - + { + const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true) + return decimalString(value, value >= 10 ? 0 : 1) + " " + unit + }} + contentFormatter={({ value }) => { + const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, true) + return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit + }} + /> - + { + let { value, unit } = formatBytes(val, true, userSettings.unitNet, true) + // value = value >= 10 ? Math.ceil(value) : value + return decimalString(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 + }} + /> {containerFilterBar && containerData.length > 0 && ( @@ -594,10 +627,6 @@ export default function SystemDetail({ name }: { name: string }) {
{Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => { const gpu = systemStats.at(-1)?.stats.g?.[id] as GPUData - const sizeFormatter = (value: number, decimals?: number) => { - const { v, u } = getSizeAndUnit(value, false) - return toFixedFloat(v, decimals || 1) + u - } return (
- + toFixedWithoutTrailingZeros(val, 2) + "%"} + contentFormatter={({ value }) => decimalString(value) + "%"} + /> sizeFormatter(value, 2)} + tickFormatter={(val) => { + const { value, unit } = formatBytes(val, false, Unit.Bytes, true) + return decimalString(value, value >= 10 ? 0 : 1) + " " + unit + }} + contentFormatter={({ value }) => { + const { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true) + return decimalString(convertedValue) + " " + unit + }} />
@@ -653,7 +693,19 @@ export default function SystemDetail({ name }: { name: string }) { description={t`Throughput of ${extraFsName}`} cornerEl={maxValSelect} > - + { + const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true) + return decimalString(value, value >= 10 ? 0 : 1) + " " + unit + }} + contentFormatter={({ value }) => { + const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, true) + return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit + }} + />
) diff --git a/beszel/site/src/components/systems-table/systems-table.tsx b/beszel/site/src/components/systems-table/systems-table.tsx index ad7d2e0..2610d27 100644 --- a/beszel/site/src/components/systems-table/systems-table.tsx +++ b/beszel/site/src/components/systems-table/systems-table.tsx @@ -68,11 +68,11 @@ import { useStore } from "@nanostores/react" import { cn, copyToClipboard, - decimalString, isReadOnlyUser, useLocalStorage, - convertTemperature, - convertNetworkSpeed, + formatTemperature, + decimalString, + formatBytes, } from "@/lib/utils" import AlertsButton from "../alerts/alert-button" import { $router, Link, navigate } from "../router" @@ -135,6 +135,7 @@ export default function SystemsTable() { const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useLocalStorage("cols", {}) const [viewMode, setViewMode] = useLocalStorage("viewMode", window.innerWidth > 1024 ? "table" : "grid") + const userSettings = useStore($userSettings) const locale = i18n.locale @@ -225,14 +226,16 @@ export default function SystemsTable() { accessorFn: (originalRow) => originalRow.info.b || 0, id: "net", name: () => t`Net`, - size: 50, + size: 0, Icon: EthernetIcon, header: sortableHeader, cell(info) { - const val = info.getValue() as number - const userSettings = useStore($userSettings) - const { display } = convertNetworkSpeed(val, userSettings.networkUnit) - return {display} + const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, true) + return ( + + {decimalString(value, value >= 100 ? 1 : 2)} {unit} + + ) }, }, { @@ -288,11 +291,10 @@ export default function SystemsTable() { if (!val) { return null } - const userSettings = useStore($userSettings) - const { value, symbol } = convertTemperature(val, userSettings.temperatureUnit) + const { value, unit } = formatTemperature(val, userSettings.unitTemp) return ( - {decimalString(value, value >= 100 ? 1 : 2)} {symbol} + {decimalString(value, value >= 100 ? 1 : 2)} {unit} ) }, @@ -765,4 +767,4 @@ function IndicatorDot({ system, className }: { system: SystemRecord; className?: // style={{ marginBottom: "-1px" }} /> ) -} \ No newline at end of file +} diff --git a/beszel/site/src/lib/enums.ts b/beszel/site/src/lib/enums.ts index ded663a..fc7f45e 100644 --- a/beszel/site/src/lib/enums.ts +++ b/beszel/site/src/lib/enums.ts @@ -1,3 +1,4 @@ +/** Operating system */ export enum Os { Linux = 0, Darwin, @@ -5,9 +6,18 @@ export enum Os { FreeBSD, } +/** Type of chart */ export enum ChartType { Memory, Disk, Network, CPU, } + +/** Unit of measurement */ +export enum Unit { + Bytes, + Bits, + Celsius, + Fahrenheit, +} diff --git a/beszel/site/src/lib/stores.ts b/beszel/site/src/lib/stores.ts index 840ee13..de44441 100644 --- a/beszel/site/src/lib/stores.ts +++ b/beszel/site/src/lib/stores.ts @@ -28,9 +28,9 @@ export const $maxValues = atom(false) export const $userSettings = map({ chartTime: "1h", emails: [pb.authStore.record?.email || ""], - temperatureUnit: "celsius", - networkUnit: "mbps", - diskUnit: "mbps", + // unitTemp: "celsius", + // unitNet: "mbps", + // unitDisk: "mbps", }) // update local storage on change $userSettings.subscribe((value) => { diff --git a/beszel/site/src/lib/utils.ts b/beszel/site/src/lib/utils.ts index 88a9181..d8e8c35 100644 --- a/beszel/site/src/lib/utils.ts +++ b/beszel/site/src/lib/utils.ts @@ -3,7 +3,7 @@ import { toast } from "@/components/ui/use-toast" import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores" -import { AlertInfo, AlertRecord, ChartTimeData, ChartTimes, FingerprintRecord, SystemRecord, TemperatureUnit, TemperatureConversion, SpeedUnit, SpeedConversion } from "@/types" +import { AlertInfo, AlertRecord, ChartTimeData, ChartTimes, FingerprintRecord, SystemRecord } from "@/types" import { RecordModel, RecordSubscription } from "pocketbase" import { WritableAtom } from "nanostores" import { timeDay, timeHour } from "d3-time" @@ -11,6 +11,7 @@ import { useEffect, useState } from "react" import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react" import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons" import { prependBasePath } from "@/components/router" +import { Unit } from "./enums" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -230,12 +231,15 @@ export function toFixedWithoutTrailingZeros(num: number, digits: number) { } export function toFixedFloat(num: number, digits: number) { - return parseFloat(num.toFixed(digits)) + return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits)) } let decimalFormatters: Map = new Map() /** Format number to x decimal places */ export function decimalString(num: number, digits = 2) { + if (digits === 0) { + return Math.ceil(num).toString() + } let formatter = decimalFormatters.get(digits) if (!formatter) { formatter = new Intl.NumberFormat(undefined, { @@ -266,143 +270,93 @@ export function useLocalStorage(key: string, defaultValue: T) { return [value, setValue] } -/** Convert temperature from Celsius to the specified unit */ -export function convertTemperature( - celsius: number, - unit: TemperatureUnit = "celsius" -): TemperatureConversion { - switch (unit) { - case "fahrenheit": - return { value: (celsius * 9) / 5 + 32, symbol: "°F" } - default: - return { value: celsius, symbol: "°C" } +/** Format temperature to user's preferred unit */ +export function formatTemperature(celsius: number, unit?: Unit): { value: number; unit: string } { + if (!unit) { + unit = $userSettings.get().unitTemp || Unit.Celsius } -} - -/** Convert network speed from MB/s to the specified unit */ -export function convertNetworkSpeed( - mbps: number, - unit: SpeedUnit = "mbps" -): SpeedConversion { - switch (unit) { - case "bps": { - const bps = mbps * 8 * 1_000_000 // Convert MB/s to bits per second - - // Format large numbers appropriately - if (bps >= 1_000_000_000) { - return { - value: bps / 1_000_000_000, - symbol: " Gbps", - display: `${decimalString(bps / 1_000_000_000, bps >= 10_000_000_000 ? 0 : 1)} Gbps`, - } - } else if (bps >= 1_000_000) { - return { - value: bps / 1_000_000, - symbol: " Mbps", - display: `${decimalString(bps / 1_000_000, bps >= 10_000_000 ? 0 : 1)} Mbps`, - } - } else if (bps >= 1_000) { - return { - value: bps / 1_000, - symbol: " Kbps", - display: `${decimalString(bps / 1_000, bps >= 10_000 ? 0 : 1)} Kbps`, - } - } else { - return { - value: bps, - symbol: " bps", - display: `${Math.round(bps)} bps`, - } - } + // need loose equality check due to form data being strings + if (unit == Unit.Fahrenheit) { + return { + value: celsius * 1.8 + 32, + unit: "°F", } - default: - return { - value: mbps, - symbol: " MB/s", - display: `${decimalString(mbps, mbps >= 100 ? 1 : 2)} MB/s`, - } + } + return { + value: celsius, + unit: "°C", } } -/** Convert disk speed from MB/s to the specified unit */ -export function convertDiskSpeed( - mbps: number, - unit: SpeedUnit = "mbps" -): SpeedConversion { - switch (unit) { - case "bps": { - const bps = mbps * 8 * 1_000_000 // Convert MB/s to bits per second +/** Format bytes to user's preferred unit */ +export function formatBytes( + size: number, + perSecond = false, + unit = Unit.Bytes, + isMegabytes = false +): { value: number; unit: string } { + // Convert MB to bytes if isMegabytes is true + if (isMegabytes) size *= 1024 * 1024 - // Format large numbers appropriately - if (bps >= 1_000_000_000) { - return { - value: bps / 1_000_000_000, - symbol: " Gbps", - display: `${decimalString(bps / 1_000_000_000, bps >= 10_000_000_000 ? 0 : 1)} Gbps`, - } - } else if (bps >= 1_000_000) { - return { - value: bps / 1_000_000, - symbol: " Mbps", - display: `${decimalString(bps / 1_000_000, bps >= 10_000_000 ? 0 : 1)} Mbps`, - } - } else if (bps >= 1_000) { - return { - value: bps / 1_000, - symbol: " Kbps", - display: `${decimalString(bps / 1_000, bps >= 10_000 ? 0 : 1)} Kbps`, - } - } else { - return { - value: bps, - symbol: " bps", - display: `${Math.round(bps)} bps`, - } + // need loose equality check due to form data being strings + if (unit == Unit.Bits) { + const bits = size * 8 + const suffix = perSecond ? "ps" : "" + if (bits < 1000) return { value: bits, unit: `b${suffix}` } + if (bits < 1_000_000) return { value: bits / 1_000, unit: `Kb${suffix}` } + if (bits < 1_000_000_000) + return { + value: bits / 1_000_000, + unit: `Mb${suffix}`, } + if (bits < 1_000_000_000_000) + return { + value: bits / 1_000_000_000, + unit: `Gb${suffix}`, + } + return { + value: bits / 1_000_000_000_000, + unit: `Tb${suffix}`, } - default: - return { - value: mbps, - symbol: " MB/s", - display: `${decimalString(mbps, mbps >= 100 ? 1 : 2)} MB/s`, - } + } + // bytes + const suffix = perSecond ? "/s" : "" + if (size < 100) return { value: size, unit: `B${suffix}` } + if (size < 1000 * 1024) return { value: size / 1024, unit: `KB${suffix}` } + if (size < 1000 * 1024 ** 2) + return { + value: size / 1024 ** 2, + unit: `MB${suffix}`, + } + if (size < 1000 * 1024 ** 3) + return { + value: size / 1024 ** 3, + unit: `GB${suffix}`, + } + return { + value: size / 1024 ** 4, + unit: `TB${suffix}`, } } +/** Fetch or create user settings in database */ export async function updateUserSettings() { try { const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" }) $userSettings.set(req.settings) return } catch (e) { - console.log("get settings", e) + console.error("get settings", e) } // create user settings if error fetching existing try { const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id }) $userSettings.set(createdSettings.settings) } catch (e) { - console.log("create settings", e) + console.error("create settings", e) } } -/** - * Get the value and unit of size (TB, GB, or MB) for a given size - * @param n size in gigabytes or megabytes - * @param isGigabytes boolean indicating if n represents gigabytes (true) or megabytes (false) - * @returns an object containing the value and unit of size - */ -export const getSizeAndUnit = (n: number, isGigabytes = true) => { - const sizeInGB = isGigabytes ? n : n / 1_000 - - if (sizeInGB >= 1_000) { - return { v: sizeInGB / 1_000, u: " TB" } - } else if (sizeInGB >= 1) { - return { v: sizeInGB, u: " GB" } - } - return { v: isGigabytes ? sizeInGB * 1_000 : n, u: " MB" } -} - export const chartMargin = { top: 12 } export const alertInfo: Record = { diff --git a/beszel/site/src/types.d.ts b/beszel/site/src/types.d.ts index cea3567..85ea620 100644 --- a/beszel/site/src/types.d.ts +++ b/beszel/site/src/types.d.ts @@ -1,5 +1,5 @@ import { RecordModel } from "pocketbase" -import { Os } from "./lib/enums" +import { Unit, Os } from "./lib/enums" // global window properties declare global { @@ -22,22 +22,6 @@ export interface FingerprintRecord extends RecordModel { } } -// Unit preference types -export type TemperatureUnit = "celsius" | "fahrenheit" -export type SpeedUnit = "mbps" | "bps" - -// Unit conversion result types -export interface TemperatureConversion { - value: number - symbol: string -} - -export interface SpeedConversion { - value: number - symbol: string - display: string -} - export interface SystemRecord extends RecordModel { name: string host: string @@ -221,9 +205,9 @@ export type UserSettings = { chartTime: ChartTimes emails?: string[] webhooks?: string[] - temperatureUnit?: TemperatureUnit - networkUnit?: SpeedUnit - diskUnit?: SpeedUnit + unitTemp?: Unit + unitNet?: Unit + unitDisk?: Unit } type ChartDataContainer = { diff --git a/beszel/site/tsconfig.app.json b/beszel/site/tsconfig.app.json index 3d4d173..6e897a9 100644 --- a/beszel/site/tsconfig.app.json +++ b/beszel/site/tsconfig.app.json @@ -2,10 +2,10 @@ "compilerOptions": { "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2021", + "target": "ES2022", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ES2021", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "skipLibCheck": true, "baseUrl": ".", "paths": {