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
This commit is contained in:
Anish Chanda
2025-07-14 13:46:13 -05:00
committed by GitHub
parent 926e807020
commit 6576141f54
9 changed files with 268 additions and 18 deletions

View File

@@ -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"
/>

View File

@@ -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 (
<span className="flex">
{decimalString(received)} MB/s
{receivedDisplay}
<span className="opacity-70 ms-0.5"> rx </span>
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
{decimalString(sent)} MB/s
{sentDisplay}
<span className="opacity-70 ms-0.5"> tx</span>
</span>
)

View File

@@ -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<string, string>
}
const tempSums = {} as Record<string, number>
const unit = userSettings.temperatureUnit || "celsius"
for (let data of chartData.systemStats) {
let newData = { created: data.created } as Record<string, number | string>
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={
<ChartTooltipContent
labelFormatter={(_, data) => 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}
/>
}

View File

@@ -101,6 +101,63 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</p>
</div>
<Separator />
<div className="space-y-2">
<div className="mb-4">
<h3 className="mb-1 text-lg font-medium">
<Trans>Unit preferences</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>Adjust Display units for metrics.</Trans>
</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="block" htmlFor="temperatureUnit">
<Trans>Temperature unit</Trans>
</Label>
<Select name="temperatureUnit" key={userSettings.temperatureUnit} defaultValue={userSettings.temperatureUnit || "celsius"}>
<SelectTrigger id="temperatureUnit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="celsius">Celsius (°C)</SelectItem>
<SelectItem value="fahrenheit">Fahrenheit (°F)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="block" htmlFor="networkUnit">
<Trans>Network unit</Trans>
</Label>
<Select name="networkUnit" key={userSettings.networkUnit} defaultValue={userSettings.networkUnit || "mbps"}>
<SelectTrigger id="networkUnit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="mbps">MB/s (Megabytes per second)</SelectItem>
<SelectItem value="bps">bps (Bits per second)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="block" htmlFor="diskUnit">
<Trans>Disk unit</Trans>
</Label>
<Select name="diskUnit" key={userSettings.diskUnit} defaultValue={userSettings.diskUnit || "mbps"}>
<SelectTrigger id="diskUnit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="mbps">MB/s (Megabytes per second)</SelectItem>
<SelectItem value="bps">bps (Bits per second)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<Separator />
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
<Trans>Save Settings</Trans>

View File

@@ -63,9 +63,17 @@ import {
PenBoxIcon,
} from "lucide-react"
import { memo, useEffect, useMemo, useRef, useState } from "react"
import { $systems, pb } from "@/lib/stores"
import { $systems, $userSettings, pb } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
import {
cn,
copyToClipboard,
decimalString,
isReadOnlyUser,
useLocalStorage,
convertTemperature,
convertNetworkSpeed,
} from "@/lib/utils"
import AlertsButton from "../alerts/alert-button"
import { $router, Link, navigate } from "../router"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
@@ -222,7 +230,9 @@ export default function SystemsTable() {
header: sortableHeader,
cell(info) {
const val = info.getValue() as number
return <span className="tabular-nums whitespace-nowrap">{decimalString(val, val >= 100 ? 1 : 2)} MB/s</span>
const userSettings = useStore($userSettings)
const { display } = convertNetworkSpeed(val, userSettings.networkUnit)
return <span className="tabular-nums whitespace-nowrap">{display}</span>
},
},
{
@@ -278,9 +288,11 @@ export default function SystemsTable() {
if (!val) {
return null
}
const userSettings = useStore($userSettings)
const { value, symbol } = convertTemperature(val, userSettings.temperatureUnit)
return (
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-0.5")}>
{decimalString(val)} °C
{decimalString(value, value >= 100 ? 1 : 2)} {symbol}
</span>
)
},
@@ -753,4 +765,4 @@ function IndicatorDot({ system, className }: { system: SystemRecord; className?:
// style={{ marginBottom: "-1px" }}
/>
)
}
}