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

@@ -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<number, Intl.NumberFormat> = 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<T>(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<string, AlertInfo> = {