mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
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:
@@ -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 {
|
||||||
|
@@ -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"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@@ -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"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
)
|
)
|
||||||
|
@@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@@ -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,
|
||||||
|
}
|
||||||
|
@@ -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) => {
|
||||||
|
@@ -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) {
|
// need loose equality check due to form data being strings
|
||||||
case "fahrenheit":
|
if (unit == Unit.Fahrenheit) {
|
||||||
return { value: (celsius * 9) / 5 + 32, symbol: "°F" }
|
return {
|
||||||
default:
|
value: celsius * 1.8 + 32,
|
||||||
return { value: celsius, symbol: "°C" }
|
unit: "°F",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: celsius,
|
||||||
|
unit: "°C",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert network speed from MB/s to the specified unit */
|
/** Format bytes to user's preferred unit */
|
||||||
export function convertNetworkSpeed(
|
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) {
|
||||||
|
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 {
|
return {
|
||||||
value: bps / 1_000_000_000,
|
value: bits / 1_000_000,
|
||||||
symbol: " Gbps",
|
unit: `Mb${suffix}`,
|
||||||
display: `${decimalString(bps / 1_000_000_000, bps >= 10_000_000_000 ? 0 : 1)} Gbps`,
|
|
||||||
}
|
}
|
||||||
} else if (bps >= 1_000_000) {
|
if (bits < 1_000_000_000_000)
|
||||||
return {
|
return {
|
||||||
value: bps / 1_000_000,
|
value: bits / 1_000_000_000,
|
||||||
symbol: " Mbps",
|
unit: `Gb${suffix}`,
|
||||||
display: `${decimalString(bps / 1_000_000, bps >= 10_000_000 ? 0 : 1)} Mbps`,
|
|
||||||
}
|
}
|
||||||
} else if (bps >= 1_000) {
|
|
||||||
return {
|
return {
|
||||||
value: bps / 1_000,
|
value: bits / 1_000_000_000_000,
|
||||||
symbol: " Kbps",
|
unit: `Tb${suffix}`,
|
||||||
display: `${decimalString(bps / 1_000, bps >= 10_000 ? 0 : 1)} Kbps`,
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
// 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 {
|
return {
|
||||||
value: bps,
|
value: size / 1024 ** 2,
|
||||||
symbol: " bps",
|
unit: `MB${suffix}`,
|
||||||
display: `${Math.round(bps)} bps`,
|
|
||||||
}
|
}
|
||||||
}
|
if (size < 1000 * 1024 ** 3)
|
||||||
}
|
|
||||||
default:
|
|
||||||
return {
|
return {
|
||||||
value: mbps,
|
value: size / 1024 ** 3,
|
||||||
symbol: " MB/s",
|
unit: `GB${suffix}`,
|
||||||
display: `${decimalString(mbps, mbps >= 100 ? 1 : 2)} MB/s`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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 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 {
|
|
||||||
value: mbps,
|
|
||||||
symbol: " MB/s",
|
|
||||||
display: `${decimalString(mbps, mbps >= 100 ? 1 : 2)} MB/s`,
|
|
||||||
}
|
}
|
||||||
|
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> = {
|
||||||
|
24
beszel/site/src/types.d.ts
vendored
24
beszel/site/src/types.d.ts
vendored
@@ -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 = {
|
||||||
|
@@ -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": {
|
||||||
|
Reference in New Issue
Block a user