Refactor unit preferences and update chart components

* Refactor user settings to use enum for unit preferences (temperature,
network, disk).
* Update chart components to utilize new unit formatting functions
* Remove deprecated conversion functions and streamline unit handling
across charts.
* Enhance settings page to allow user selection of unit preferences with
updated labels.
This commit is contained in:
henrygd
2025-07-15 18:57:37 -04:00
parent 6576141f54
commit 5c047e4afd
15 changed files with 269 additions and 305 deletions

View File

@@ -17,10 +17,9 @@ type UserSettings struct {
ChartTime string `json:"chartTime"` ChartTime string `json:"chartTime"`
NotificationEmails []string `json:"emails"` NotificationEmails []string `json:"emails"`
NotificationWebhooks []string `json:"webhooks"` NotificationWebhooks []string `json:"webhooks"`
TemperatureUnit string `json:"temperatureUnit"` // "celsius" or "fahrenheit" // UnitTemp uint8 `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit
NetworkUnit string `json:"networkUnit"` // "mbps" (MB/s) or "bps" // UnitNet uint8 `json:"unitNet"` // 0 for bytes, 1 for bits
DiskUnit string `json:"diskUnit"` // "mbps" (MB/s) or "bps" // UnitDisk uint8 `json:"unitDisk"` // 0 for bytes, 1 for bits
// Language string `json:"lang"`
} }
func NewUserManager(app core.App) *UserManager { func NewUserManager(app core.App) *UserManager {
@@ -42,13 +41,9 @@ func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {
record := e.Record record := e.Record
// intialize settings with defaults // intialize settings with defaults
settings := UserSettings{ settings := UserSettings{
// Language: "en",
ChartTime: "1h", ChartTime: "1h",
NotificationEmails: []string{}, NotificationEmails: []string{},
NotificationWebhooks: []string{}, NotificationWebhooks: []string{},
TemperatureUnit: "celsius",
NetworkUnit: "mbps",
DiskUnit: "mbps",
} }
record.UnmarshalJSONField("settings", &settings) record.UnmarshalJSONField("settings", &settings)
if len(settings.NotificationEmails) == 0 { if len(settings.NotificationEmails) == 0 {

View File

@@ -2,22 +2,11 @@ import { t } from "@lingui/core/macro"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils"
useYAxisWidth,
cn,
formatShortDate,
toFixedWithoutTrailingZeros,
decimalString,
chartMargin,
convertNetworkSpeed,
convertDiskSpeed,
} from "@/lib/utils"
// import Spinner from '../spinner' // import Spinner from '../spinner'
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { memo, useMemo } from "react" import { memo, useMemo } from "react"
import { useLingui } from "@lingui/react/macro" import { useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { $userSettings } from "@/lib/stores"
/** [label, key, color, opacity] */ /** [label, key, color, opacity] */
type DataKeys = [string, string, number, number] 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({ export default memo(function AreaChartDefault({
maxToggled = false, maxToggled = false,
unit = " MB/s",
chartName, chartName,
chartData, chartData,
max, max,
@@ -42,35 +30,19 @@ export default memo(function AreaChartDefault({
contentFormatter, contentFormatter,
}: { }: {
maxToggled?: boolean maxToggled?: boolean
unit?: string
chartName: string chartName: string
chartData: ChartData chartData: ChartData
max?: number max?: number
tickFormatter?: (value: number) => string tickFormatter: (value: number) => string
contentFormatter?: (value: number) => string contentFormatter: ({ value }: { value: number }) => string
}) { }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { i18n } = useLingui() const { i18n } = useLingui()
const userSettings = useStore($userSettings)
const { chartTime } = chartData const { chartTime } = chartData
const showMax = chartTime !== "1h" && maxToggled 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(() => { const dataKeys: DataKeys[] = useMemo(() => {
// [label, key, color, opacity] // [label, key, color, opacity]
if (chartName === "CPU Usage") { if (chartName === "CPU Usage") {
@@ -117,21 +89,7 @@ export default memo(function AreaChartDefault({
className="tracking-tighter" className="tracking-tighter"
width={yAxisWidth} width={yAxisWidth}
domain={[0, max ?? "auto"]} domain={[0, max ?? "auto"]}
tickFormatter={(value) => { tickFormatter={(value) => updateYAxisWidth(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)
}}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
/> />
@@ -142,18 +100,7 @@ export default memo(function AreaChartDefault({
content={ content={
<ChartTooltipContent <ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={({ value }) => { contentFormatter={contentFormatter}
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
}}
// indicator="line" // indicator="line"
/> />
} }

View File

@@ -5,19 +5,17 @@ import {
useYAxisWidth, useYAxisWidth,
cn, cn,
formatShortDate, formatShortDate,
decimalString,
chartMargin, chartMargin,
toFixedFloat,
getSizeAndUnit,
toFixedWithoutTrailingZeros, toFixedWithoutTrailingZeros,
convertNetworkSpeed, formatBytes,
decimalString,
} from "@/lib/utils" } from "@/lib/utils"
// import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { $containerFilter, $userSettings } from "@/lib/stores" import { $containerFilter, $userSettings } from "@/lib/stores"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
import { ChartType } from "@/lib/enums" import { ChartType, Unit } from "@/lib/enums"
export default memo(function ContainerChart({ export default memo(function ContainerChart({
dataKey, dataKey,
@@ -31,7 +29,7 @@ export default memo(function ContainerChart({
unit?: string unit?: string
}) { }) {
const filter = useStore($containerFilter) const filter = useStore($containerFilter)
const userSettings = useStore($userSettings) const userSettings = $userSettings.get()
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { containerData } = chartData const { containerData } = chartData
@@ -89,15 +87,11 @@ export default memo(function ContainerChart({
const val = toFixedWithoutTrailingZeros(value, 2) + unit const val = toFixedWithoutTrailingZeros(value, 2) + unit
return updateYAxisWidth(val) return updateYAxisWidth(val)
} }
} else if (isNetChart) {
obj.tickFormatter = (value) => {
const { value: convertedValue, symbol } = convertNetworkSpeed(value, userSettings.networkUnit)
return updateYAxisWidth(`${toFixedFloat(convertedValue, 2)}${symbol}`)
}
} else { } else {
obj.tickFormatter = (value) => { const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes
const { v, u } = getSizeAndUnit(value, false) obj.tickFormatter = (val) => {
return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}`) const { value, unit } = formatBytes(val, isNetChart, chartUnit, true)
return updateYAxisWidth(decimalString(value, value >= 10 ? 0 : 1) + " " + unit)
} }
} }
// tooltip formatter // tooltip formatter
@@ -106,14 +100,14 @@ export default memo(function ContainerChart({
try { try {
const sent = item?.payload?.[key]?.ns ?? 0 const sent = item?.payload?.[key]?.ns ?? 0
const received = item?.payload?.[key]?.nr ?? 0 const received = item?.payload?.[key]?.nr ?? 0
const { display: receivedDisplay } = convertNetworkSpeed(received, userSettings.networkUnit) const { value: receivedValue, unit: receivedUnit } = formatBytes(received, true, userSettings.unitNet, true)
const { display: sentDisplay } = convertNetworkSpeed(sent, userSettings.networkUnit) const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, true)
return ( return (
<span className="flex"> <span className="flex">
{receivedDisplay} {decimalString(receivedValue)} {receivedUnit}
<span className="opacity-70 ms-0.5"> rx </span> <span className="opacity-70 ms-0.5"> rx </span>
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" /> <Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
{sentDisplay} {decimalString(sentValue)} {sentUnit}
<span className="opacity-70 ms-0.5"> tx</span> <span className="opacity-70 ms-0.5"> tx</span>
</span> </span>
) )
@@ -123,8 +117,8 @@ export default memo(function ContainerChart({
} }
} else if (chartType === ChartType.Memory) { } else if (chartType === ChartType.Memory) {
obj.toolTipFormatter = (item: any) => { obj.toolTipFormatter = (item: any) => {
const { v, u } = getSizeAndUnit(item.value, false) const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
return decimalString(v, 2) + u return decimalString(value) + " " + unit
} }
} else { } else {
obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit

View File

@@ -1,17 +1,10 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes } from "@/lib/utils"
useYAxisWidth,
cn,
formatShortDate,
decimalString,
toFixedFloat,
chartMargin,
getSizeAndUnit,
} from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { memo } from "react" import { memo } from "react"
import { useLingui } from "@lingui/react/macro" import { useLingui } from "@lingui/react/macro"
import { Unit } from "@/lib/enums"
export default memo(function DiskChart({ export default memo(function DiskChart({
dataKey, dataKey,
@@ -53,9 +46,9 @@ export default memo(function DiskChart({
minTickGap={6} minTickGap={6}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickFormatter={(value) => { tickFormatter={(val) => {
const { v, u } = getSizeAndUnit(value) const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
return updateYAxisWidth(toFixedFloat(v, 2) + u) return updateYAxisWidth(decimalString(value, value >= 10 ? 0 : 1) + " " + unit)
}} }}
/> />
{xAxis(chartData)} {xAxis(chartData)}
@@ -66,8 +59,8 @@ export default memo(function DiskChart({
<ChartTooltipContent <ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={({ value }) => { contentFormatter={({ value }) => {
const { v, u } = getSizeAndUnit(value) const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return decimalString(v) + u return decimalString(convertedValue) + " " + unit
}} }}
/> />
} }

View File

@@ -1,9 +1,10 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" 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 { memo } from "react"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { useLingui } from "@lingui/react/macro" import { useLingui } from "@lingui/react/macro"
import { Unit } from "@/lib/enums"
export default memo(function MemChart({ chartData }: { chartData: ChartData }) { export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
@@ -39,8 +40,8 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickFormatter={(value) => { tickFormatter={(value) => {
const val = toFixedFloat(value, 1) const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return updateYAxisWidth(val + " GB") return updateYAxisWidth(decimalString(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
}} }}
/> />
)} )}
@@ -54,8 +55,11 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
// @ts-ignore // @ts-ignore
itemSorter={(a, b) => a.order - b.order} itemSorter={(a, b) => a.order - b.order}
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => decimalString(item.value) + " GB"} contentFormatter={({ value }) => {
// indicator="line" // 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
}}
/> />
} }
/> />

View File

@@ -1,4 +1,4 @@
import { t } from "@lingui/core/macro"; import { t } from "@lingui/core/macro"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
@@ -9,12 +9,15 @@ import {
toFixedWithoutTrailingZeros, toFixedWithoutTrailingZeros,
decimalString, decimalString,
chartMargin, chartMargin,
formatBytes,
} from "@/lib/utils" } from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { memo } from "react" import { memo } from "react"
import { $userSettings } from "@/lib/stores"
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) { export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const userSettings = $userSettings.get()
if (chartData.systemStats.length === 0) { if (chartData.systemStats.length === 0) {
return null return null
@@ -37,7 +40,10 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
width={yAxisWidth} width={yAxisWidth}
tickLine={false} tickLine={false}
axisLine={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)} {xAxis(chartData)}
<ChartTooltip <ChartTooltip
@@ -46,7 +52,11 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
content={ content={
<ChartTooltipContent <ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => 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" // indicator="line"
/> />
} }

View File

@@ -13,9 +13,9 @@ import {
cn, cn,
formatShortDate, formatShortDate,
toFixedWithoutTrailingZeros, toFixedWithoutTrailingZeros,
decimalString,
chartMargin, chartMargin,
convertTemperature, formatTemperature,
decimalString,
} from "@/lib/utils" } from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { memo, useMemo } from "react" import { memo, useMemo } from "react"
@@ -38,17 +38,13 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
colors: Record<string, string> colors: Record<string, string>
} }
const tempSums = {} as Record<string, number> const tempSums = {} as Record<string, number>
const unit = userSettings.temperatureUnit || "celsius"
for (let data of chartData.systemStats) { for (let data of chartData.systemStats) {
let newData = { created: data.created } as Record<string, number | string> let newData = { created: data.created } as Record<string, number | string>
let keys = Object.keys(data.stats?.t ?? {}) let keys = Object.keys(data.stats?.t ?? {})
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
let key = keys[i] let key = keys[i]
const celsiusTemp = data.stats.t![key] newData[key] = data.stats.t![key]
const { value } = convertTemperature(celsiusTemp, unit) tempSums[key] = (tempSums[key] ?? 0) + newData[key]
newData[key] = value
tempSums[key] = (tempSums[key] ?? 0) + value
} }
newChartData.data.push(newData) 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%)` newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
} }
return newChartData return newChartData
}, [chartData, userSettings.temperatureUnit]) }, [chartData])
const colors = Object.keys(newChartData.colors) const colors = Object.keys(newChartData.colors)
@@ -78,10 +74,9 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
className="tracking-tighter" className="tracking-tighter"
domain={[0, "auto"]} domain={[0, "auto"]}
width={yAxisWidth} width={yAxisWidth}
tickFormatter={(value) => { tickFormatter={(val) => {
const val = toFixedWithoutTrailingZeros(value, 2) const { value, unit } = formatTemperature(val, userSettings.unitTemp)
const { symbol } = convertTemperature(0, userSettings.temperatureUnit || "celsius") return updateYAxisWidth(toFixedWithoutTrailingZeros(value, 2) + " " + unit)
return updateYAxisWidth(val + " " + symbol)
}} }}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
@@ -96,8 +91,8 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
<ChartTooltipContent <ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={(item) => { contentFormatter={(item) => {
const { symbol } = convertTemperature(0, userSettings.temperatureUnit || "celsius") const { value, unit } = formatTemperature(item.value, userSettings.unitTemp)
return decimalString(item.value) + " " + symbol return decimalString(value) + " " + unit
}} }}
filter={filter} filter={filter}
/> />

View File

@@ -11,7 +11,7 @@ import { useState } from "react"
import languages from "@/lib/languages" import languages from "@/lib/languages"
import { dynamicActivate } from "@/lib/i18n" import { dynamicActivate } from "@/lib/i18n"
import { useLingui } from "@lingui/react/macro" import { useLingui } from "@lingui/react/macro"
// import { setLang } from "@/lib/i18n" import { Unit } from "@/lib/enums"
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) { export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@@ -107,51 +107,75 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
<Trans>Unit preferences</Trans> <Trans>Unit preferences</Trans>
</h3> </h3>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
<Trans>Adjust Display units for metrics.</Trans> <Trans>Change display units for metrics.</Trans>
</p> </p>
</div> </div>
<div className="space-y-4"> <div className="grid sm:grid-cols-3 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="block" htmlFor="temperatureUnit"> <Label className="block" htmlFor="unitTemp">
<Trans>Temperature unit</Trans> <Trans>Temperature unit</Trans>
</Label> </Label>
<Select name="temperatureUnit" key={userSettings.temperatureUnit} defaultValue={userSettings.temperatureUnit || "celsius"}> <Select
<SelectTrigger id="temperatureUnit"> name="unitTemp"
key={userSettings.unitTemp}
defaultValue={userSettings.unitTemp?.toString() || String(Unit.Celsius)}
>
<SelectTrigger id="unitTemp">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="celsius">Celsius (°C)</SelectItem> <SelectItem value={String(Unit.Celsius)}>
<SelectItem value="fahrenheit">Fahrenheit (°F)</SelectItem> <Trans>Celsius (°C)</Trans>
</SelectItem>
<SelectItem value={String(Unit.Fahrenheit)}>
<Trans>Fahrenheit (°F)</Trans>
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="block" htmlFor="networkUnit"> <Label className="block" htmlFor="unitNet">
<Trans>Network unit</Trans> <Trans>Network unit</Trans>
</Label> </Label>
<Select name="networkUnit" key={userSettings.networkUnit} defaultValue={userSettings.networkUnit || "mbps"}> <Select
<SelectTrigger id="networkUnit"> name="unitNet"
key={userSettings.unitNet}
defaultValue={userSettings.unitNet?.toString() ?? String(Unit.Bytes)}
>
<SelectTrigger id="unitNet">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="mbps">MB/s (Megabytes per second)</SelectItem> <SelectItem value={String(Unit.Bytes)}>
<SelectItem value="bps">bps (Bits per second)</SelectItem> <Trans>Bytes (KB/s, MB/s, GB/s)</Trans>
</SelectItem>
<SelectItem value={String(Unit.Bits)}>
<Trans>Bits (Kbps, Mbps, Gbps)</Trans>
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="block" htmlFor="diskUnit"> <Label className="block" htmlFor="unitDisk">
<Trans>Disk unit</Trans> <Trans>Disk unit</Trans>
</Label> </Label>
<Select name="diskUnit" key={userSettings.diskUnit} defaultValue={userSettings.diskUnit || "mbps"}> <Select
<SelectTrigger id="diskUnit"> name="unitDisk"
key={userSettings.unitDisk}
defaultValue={userSettings.unitDisk?.toString() ?? String(Unit.Bytes)}
>
<SelectTrigger id="unitDisk">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="mbps">MB/s (Megabytes per second)</SelectItem> <SelectItem value={String(Unit.Bytes)}>
<SelectItem value="bps">bps (Bits per second)</SelectItem> <Trans>Bytes (KB/s, MB/s, GB/s)</Trans>
</SelectItem>
<SelectItem value={String(Unit.Bits)}>
<Trans>Bits (Kbps, Mbps, Gbps)</Trans>
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@@ -11,7 +11,7 @@ import {
$temperatureFilter, $temperatureFilter,
} from "@/lib/stores" } from "@/lib/stores"
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types" 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 React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card" import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
@@ -21,11 +21,12 @@ import ChartTimeSelect from "../charts/chart-time-select"
import { import {
chartTimeData, chartTimeData,
cn, cn,
decimalString,
formatBytes,
getHostDisplayValue, getHostDisplayValue,
getPbTimestamp, getPbTimestamp,
getSizeAndUnit,
listen, listen,
toFixedFloat, toFixedWithoutTrailingZeros,
useLocalStorage, useLocalStorage,
} from "@/lib/utils" } from "@/lib/utils"
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
@@ -131,6 +132,7 @@ export default function SystemDetail({ name }: { name: string }) {
const [bottomSpacing, setBottomSpacing] = useState(0) const [bottomSpacing, setBottomSpacing] = useState(0)
const [chartLoading, setChartLoading] = useState(true) const [chartLoading, setChartLoading] = useState(true)
const isLongerChart = chartTime !== "1h" const isLongerChart = chartTime !== "1h"
const userSettings = $userSettings.get()
useEffect(() => { useEffect(() => {
document.title = `${name} / Beszel` document.title = `${name} / Beszel`
@@ -472,7 +474,13 @@ export default function SystemDetail({ name }: { name: string }) {
description={t`Average system-wide CPU utilization`} description={t`Average system-wide CPU utilization`}
cornerEl={maxValSelect} cornerEl={maxValSelect}
> >
<AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={maxValues} unit="%" /> <AreaChartDefault
chartData={chartData}
chartName="CPU Usage"
maxToggled={maxValues}
tickFormatter={(val) => toFixedWithoutTrailingZeros(val, 2) + "%"}
contentFormatter={({ value }) => decimalString(value) + "%"}
/>
</ChartCard> </ChartCard>
{containerFilterBar && ( {containerFilterBar && (
@@ -519,7 +527,19 @@ export default function SystemDetail({ name }: { name: string }) {
description={t`Throughput of root filesystem`} description={t`Throughput of root filesystem`}
cornerEl={maxValSelect} cornerEl={maxValSelect}
> >
<AreaChartDefault chartData={chartData} chartName="dio" maxToggled={maxValues} /> <AreaChartDefault
chartData={chartData}
chartName="dio"
maxToggled={maxValues}
tickFormatter={(val) => {
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
}}
/>
</ChartCard> </ChartCard>
<ChartCard <ChartCard
@@ -529,7 +549,20 @@ export default function SystemDetail({ name }: { name: string }) {
cornerEl={maxValSelect} cornerEl={maxValSelect}
description={t`Network traffic of public interfaces`} description={t`Network traffic of public interfaces`}
> >
<AreaChartDefault chartData={chartData} chartName="bw" maxToggled={maxValues} /> <AreaChartDefault
chartData={chartData}
chartName="bw"
maxToggled={maxValues}
tickFormatter={(val) => {
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
}}
/>
</ChartCard> </ChartCard>
{containerFilterBar && containerData.length > 0 && ( {containerFilterBar && containerData.length > 0 && (
@@ -594,10 +627,6 @@ export default function SystemDetail({ name }: { name: string }) {
<div className="grid xl:grid-cols-2 gap-4"> <div className="grid xl:grid-cols-2 gap-4">
{Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => { {Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => {
const gpu = systemStats.at(-1)?.stats.g?.[id] as GPUData 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 ( return (
<div key={id} className="contents"> <div key={id} className="contents">
<ChartCard <ChartCard
@@ -606,7 +635,12 @@ export default function SystemDetail({ name }: { name: string }) {
title={`${gpu.n} ${t`Usage`}`} title={`${gpu.n} ${t`Usage`}`}
description={t`Average utilization of ${gpu.n}`} description={t`Average utilization of ${gpu.n}`}
> >
<AreaChartDefault chartData={chartData} chartName={`g.${id}.u`} unit="%" /> <AreaChartDefault
chartData={chartData}
chartName={`g.${id}.u`}
tickFormatter={(val) => toFixedWithoutTrailingZeros(val, 2) + "%"}
contentFormatter={({ value }) => decimalString(value) + "%"}
/>
</ChartCard> </ChartCard>
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
@@ -618,8 +652,14 @@ export default function SystemDetail({ name }: { name: string }) {
chartData={chartData} chartData={chartData}
chartName={`g.${id}.mu`} chartName={`g.${id}.mu`}
max={gpu.mt} max={gpu.mt}
tickFormatter={sizeFormatter} tickFormatter={(val) => {
contentFormatter={(value) => sizeFormatter(value, 2)} 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
}}
/> />
</ChartCard> </ChartCard>
</div> </div>
@@ -653,7 +693,19 @@ export default function SystemDetail({ name }: { name: string }) {
description={t`Throughput of ${extraFsName}`} description={t`Throughput of ${extraFsName}`}
cornerEl={maxValSelect} cornerEl={maxValSelect}
> >
<AreaChartDefault chartData={chartData} chartName={`efs.${extraFsName}`} maxToggled={maxValues} /> <AreaChartDefault
chartData={chartData}
chartName={`efs.${extraFsName}`}
maxToggled={maxValues}
tickFormatter={(val) => {
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
}}
/>
</ChartCard> </ChartCard>
</div> </div>
) )

View File

@@ -68,11 +68,11 @@ import { useStore } from "@nanostores/react"
import { import {
cn, cn,
copyToClipboard, copyToClipboard,
decimalString,
isReadOnlyUser, isReadOnlyUser,
useLocalStorage, useLocalStorage,
convertTemperature, formatTemperature,
convertNetworkSpeed, decimalString,
formatBytes,
} from "@/lib/utils" } from "@/lib/utils"
import AlertsButton from "../alerts/alert-button" import AlertsButton from "../alerts/alert-button"
import { $router, Link, navigate } from "../router" import { $router, Link, navigate } from "../router"
@@ -135,6 +135,7 @@ export default function SystemsTable() {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {}) const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid") const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
const userSettings = useStore($userSettings)
const locale = i18n.locale const locale = i18n.locale
@@ -225,14 +226,16 @@ export default function SystemsTable() {
accessorFn: (originalRow) => originalRow.info.b || 0, accessorFn: (originalRow) => originalRow.info.b || 0,
id: "net", id: "net",
name: () => t`Net`, name: () => t`Net`,
size: 50, size: 0,
Icon: EthernetIcon, Icon: EthernetIcon,
header: sortableHeader, header: sortableHeader,
cell(info) { cell(info) {
const val = info.getValue() as number const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, true)
const userSettings = useStore($userSettings) return (
const { display } = convertNetworkSpeed(val, userSettings.networkUnit) <span className="tabular-nums whitespace-nowrap">
return <span className="tabular-nums whitespace-nowrap">{display}</span> {decimalString(value, value >= 100 ? 1 : 2)} {unit}
</span>
)
}, },
}, },
{ {
@@ -288,11 +291,10 @@ export default function SystemsTable() {
if (!val) { if (!val) {
return null return null
} }
const userSettings = useStore($userSettings) const { value, unit } = formatTemperature(val, userSettings.unitTemp)
const { value, symbol } = convertTemperature(val, userSettings.temperatureUnit)
return ( return (
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-0.5")}> <span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-0.5")}>
{decimalString(value, value >= 100 ? 1 : 2)} {symbol} {decimalString(value, value >= 100 ? 1 : 2)} {unit}
</span> </span>
) )
}, },

View File

@@ -1,3 +1,4 @@
/** Operating system */
export enum Os { export enum Os {
Linux = 0, Linux = 0,
Darwin, Darwin,
@@ -5,9 +6,18 @@ export enum Os {
FreeBSD, FreeBSD,
} }
/** Type of chart */
export enum ChartType { export enum ChartType {
Memory, Memory,
Disk, Disk,
Network, Network,
CPU, CPU,
} }
/** Unit of measurement */
export enum Unit {
Bytes,
Bits,
Celsius,
Fahrenheit,
}

View File

@@ -28,9 +28,9 @@ export const $maxValues = atom(false)
export const $userSettings = map<UserSettings>({ export const $userSettings = map<UserSettings>({
chartTime: "1h", chartTime: "1h",
emails: [pb.authStore.record?.email || ""], emails: [pb.authStore.record?.email || ""],
temperatureUnit: "celsius", // unitTemp: "celsius",
networkUnit: "mbps", // unitNet: "mbps",
diskUnit: "mbps", // unitDisk: "mbps",
}) })
// update local storage on change // update local storage on change
$userSettings.subscribe((value) => { $userSettings.subscribe((value) => {

View File

@@ -3,7 +3,7 @@ import { toast } from "@/components/ui/use-toast"
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores" 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 { RecordModel, RecordSubscription } from "pocketbase"
import { WritableAtom } from "nanostores" import { WritableAtom } from "nanostores"
import { timeDay, timeHour } from "d3-time" import { timeDay, timeHour } from "d3-time"
@@ -11,6 +11,7 @@ import { useEffect, useState } from "react"
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react" import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons" import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons"
import { prependBasePath } from "@/components/router" import { prependBasePath } from "@/components/router"
import { Unit } from "./enums"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -230,12 +231,15 @@ export function toFixedWithoutTrailingZeros(num: number, digits: number) {
} }
export function toFixedFloat(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<number, Intl.NumberFormat> = new Map() let decimalFormatters: Map<number, Intl.NumberFormat> = new Map()
/** Format number to x decimal places */ /** Format number to x decimal places */
export function decimalString(num: number, digits = 2) { export function decimalString(num: number, digits = 2) {
if (digits === 0) {
return Math.ceil(num).toString()
}
let formatter = decimalFormatters.get(digits) let formatter = decimalFormatters.get(digits)
if (!formatter) { if (!formatter) {
formatter = new Intl.NumberFormat(undefined, { formatter = new Intl.NumberFormat(undefined, {
@@ -266,143 +270,93 @@ export function useLocalStorage<T>(key: string, defaultValue: T) {
return [value, setValue] return [value, setValue]
} }
/** Convert temperature from Celsius to the specified unit */ /** Format temperature to user's preferred unit */
export function convertTemperature( export function formatTemperature(celsius: number, unit?: Unit): { value: number; unit: string } {
celsius: number, if (!unit) {
unit: TemperatureUnit = "celsius" unit = $userSettings.get().unitTemp || Unit.Celsius
): TemperatureConversion {
switch (unit) {
case "fahrenheit":
return { value: (celsius * 9) / 5 + 32, symbol: "°F" }
default:
return { value: celsius, symbol: "°C" }
} }
} // need loose equality check due to form data being strings
if (unit == Unit.Fahrenheit) {
/** Convert network speed from MB/s to the specified unit */ return {
export function convertNetworkSpeed( value: celsius * 1.8 + 32,
mbps: number, unit: "°F",
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`,
}
}
} }
default: }
return { return {
value: mbps, value: celsius,
symbol: " MB/s", unit: "°C",
display: `${decimalString(mbps, mbps >= 100 ? 1 : 2)} MB/s`,
}
} }
} }
/** Convert disk speed from MB/s to the specified unit */ /** Format bytes to user's preferred unit */
export function convertDiskSpeed( export function formatBytes(
mbps: number, size: number,
unit: SpeedUnit = "mbps" perSecond = false,
): SpeedConversion { unit = Unit.Bytes,
switch (unit) { isMegabytes = false
case "bps": { ): { value: number; unit: string } {
const bps = mbps * 8 * 1_000_000 // Convert MB/s to bits per second // Convert MB to bytes if isMegabytes is true
if (isMegabytes) size *= 1024 * 1024
// Format large numbers appropriately // need loose equality check due to form data being strings
if (bps >= 1_000_000_000) { if (unit == Unit.Bits) {
return { const bits = size * 8
value: bps / 1_000_000_000, const suffix = perSecond ? "ps" : ""
symbol: " Gbps", if (bits < 1000) return { value: bits, unit: `b${suffix}` }
display: `${decimalString(bps / 1_000_000_000, bps >= 10_000_000_000 ? 0 : 1)} Gbps`, if (bits < 1_000_000) return { value: bits / 1_000, unit: `Kb${suffix}` }
} if (bits < 1_000_000_000)
} else if (bps >= 1_000_000) { return {
return { value: bits / 1_000_000,
value: bps / 1_000_000, unit: `Mb${suffix}`,
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`,
}
} }
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 { // bytes
value: mbps, const suffix = perSecond ? "/s" : ""
symbol: " MB/s", if (size < 100) return { value: size, unit: `B${suffix}` }
display: `${decimalString(mbps, mbps >= 100 ? 1 : 2)} MB/s`, 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() { export async function updateUserSettings() {
try { try {
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" }) const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
$userSettings.set(req.settings) $userSettings.set(req.settings)
return return
} catch (e) { } catch (e) {
console.log("get settings", e) console.error("get settings", e)
} }
// create user settings if error fetching existing // create user settings if error fetching existing
try { try {
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id }) const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
$userSettings.set(createdSettings.settings) $userSettings.set(createdSettings.settings)
} catch (e) { } 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 chartMargin = { top: 12 }
export const alertInfo: Record<string, AlertInfo> = { export const alertInfo: Record<string, AlertInfo> = {

View File

@@ -1,5 +1,5 @@
import { RecordModel } from "pocketbase" import { RecordModel } from "pocketbase"
import { Os } from "./lib/enums" import { Unit, Os } from "./lib/enums"
// global window properties // global window properties
declare global { 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 { export interface SystemRecord extends RecordModel {
name: string name: string
host: string host: string
@@ -221,9 +205,9 @@ export type UserSettings = {
chartTime: ChartTimes chartTime: ChartTimes
emails?: string[] emails?: string[]
webhooks?: string[] webhooks?: string[]
temperatureUnit?: TemperatureUnit unitTemp?: Unit
networkUnit?: SpeedUnit unitNet?: Unit
diskUnit?: SpeedUnit unitDisk?: Unit
} }
type ChartDataContainer = { type ChartDataContainer = {

View File

@@ -2,10 +2,10 @@
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2021", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"lib": ["ES2021", "DOM", "DOM.Iterable"], "lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {