mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 22:16:18 +01:00
Compare commits
1 Commits
svenvg93-f
...
feat/displ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d759112ee9 |
@@ -56,7 +56,7 @@ dev-hub: export ENV=dev
|
|||||||
dev-hub:
|
dev-hub:
|
||||||
mkdir -p ./site/dist && touch ./site/dist/index.html
|
mkdir -p ./site/dist && touch ./site/dist/index.html
|
||||||
@if command -v entr >/dev/null 2>&1; then \
|
@if command -v entr >/dev/null 2>&1; then \
|
||||||
find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run . serve --http 0.0.0.0:8090"; \
|
find ./cmd/hub ./internal/{alerts,hub,records,users} -name "*.go" | entr -r -s "cd ./cmd/hub && go run . serve"; \
|
||||||
else \
|
else \
|
||||||
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \
|
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/common"
|
"github.com/shirou/gopsutil/v4/common"
|
||||||
"github.com/shirou/gopsutil/v4/sensors"
|
"github.com/shirou/gopsutil/v4/sensors"
|
||||||
@@ -105,11 +103,6 @@ func (a *Agent) updateTemperatures(systemStats *system.Stats) {
|
|||||||
|
|
||||||
systemStats.Temperatures = make(map[string]float64, len(temps))
|
systemStats.Temperatures = make(map[string]float64, len(temps))
|
||||||
for i, sensor := range temps {
|
for i, sensor := range temps {
|
||||||
// check for malformed strings on darwin (gopsutil/issues/1832)
|
|
||||||
if runtime.GOOS == "darwin" && !utf8.ValidString(sensor.SensorKey) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// scale temperature
|
// scale temperature
|
||||||
if sensor.Temperature != 0 && sensor.Temperature < 1 {
|
if sensor.Temperature != 0 && sensor.Temperature < 1 {
|
||||||
sensor.Temperature = scaleTemperature(sensor.Temperature)
|
sensor.Temperature = scaleTemperature(sensor.Temperature)
|
||||||
|
|||||||
@@ -251,7 +251,6 @@ func (a *Agent) getSystemStats() system.Stats {
|
|||||||
|
|
||||||
// update base system info
|
// update base system info
|
||||||
a.systemInfo.Cpu = systemStats.Cpu
|
a.systemInfo.Cpu = systemStats.Cpu
|
||||||
a.systemInfo.LoadAvg1 = systemStats.LoadAvg1
|
|
||||||
a.systemInfo.LoadAvg5 = systemStats.LoadAvg5
|
a.systemInfo.LoadAvg5 = systemStats.LoadAvg5
|
||||||
a.systemInfo.LoadAvg15 = systemStats.LoadAvg15
|
a.systemInfo.LoadAvg15 = systemStats.LoadAvg15
|
||||||
a.systemInfo.MemPct = systemStats.MemPct
|
a.systemInfo.MemPct = systemStats.MemPct
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ type SystemAlertStats struct {
|
|||||||
NetSent float64 `json:"ns"`
|
NetSent float64 `json:"ns"`
|
||||||
NetRecv float64 `json:"nr"`
|
NetRecv float64 `json:"nr"`
|
||||||
Temperatures map[string]float32 `json:"t"`
|
Temperatures map[string]float32 `json:"t"`
|
||||||
LoadAvg1 float64 `json:"l1"`
|
|
||||||
LoadAvg5 float64 `json:"l5"`
|
LoadAvg5 float64 `json:"l5"`
|
||||||
LoadAvg15 float64 `json:"l15"`
|
LoadAvg15 float64 `json:"l15"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
}
|
}
|
||||||
val = data.Info.DashboardTemp
|
val = data.Info.DashboardTemp
|
||||||
unit = "°C"
|
unit = "°C"
|
||||||
case "LoadAvg1":
|
|
||||||
val = data.Info.LoadAvg1
|
|
||||||
unit = ""
|
|
||||||
case "LoadAvg5":
|
case "LoadAvg5":
|
||||||
val = data.Info.LoadAvg5
|
val = data.Info.LoadAvg5
|
||||||
unit = ""
|
unit = ""
|
||||||
@@ -199,8 +196,6 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
}
|
}
|
||||||
alert.mapSums[key] += temp
|
alert.mapSums[key] += temp
|
||||||
}
|
}
|
||||||
case "LoadAvg1":
|
|
||||||
alert.val += stats.LoadAvg1
|
|
||||||
case "LoadAvg5":
|
case "LoadAvg5":
|
||||||
alert.val += stats.LoadAvg5
|
alert.val += stats.LoadAvg5
|
||||||
case "LoadAvg15":
|
case "LoadAvg15":
|
||||||
|
|||||||
@@ -92,9 +92,8 @@ type Info struct {
|
|||||||
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
||||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
||||||
Os Os `json:"os" cbor:"14,keyasint"`
|
Os Os `json:"os" cbor:"14,keyasint"`
|
||||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
|
LoadAvg5 float64 `json:"l5,omitempty" cbor:"15,keyasint,omitempty,omitzero"`
|
||||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
|
LoadAvg15 float64 `json:"l15,omitempty" cbor:"16,keyasint,omitempty,omitzero"`
|
||||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final data structure to return to the hub
|
// Final data structure to return to the hub
|
||||||
|
|||||||
@@ -17,9 +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"`
|
||||||
// UnitTemp uint8 `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit
|
// TemperatureUnit int `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit
|
||||||
// UnitNet uint8 `json:"unitNet"` // 0 for bytes, 1 for bits
|
// NetworkUnit int `json:"unitNet"` // 0 for bytes, 1 for bits
|
||||||
// UnitDisk uint8 `json:"unitDisk"` // 0 for bytes, 1 for bits
|
// DiskUnit int `json:"unitDisk"` // 0 for bytes, 1 for bits
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserManager(app core.App) *UserManager {
|
func NewUserManager(app core.App) *UserManager {
|
||||||
@@ -41,6 +41,7 @@ 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{},
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ func init() {
|
|||||||
"Disk",
|
"Disk",
|
||||||
"Temperature",
|
"Temperature",
|
||||||
"Bandwidth",
|
"Bandwidth",
|
||||||
"LoadAvg1",
|
|
||||||
"LoadAvg5",
|
"LoadAvg5",
|
||||||
"LoadAvg15"
|
"LoadAvg15"
|
||||||
]
|
]
|
||||||
@@ -11,14 +11,13 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { BellIcon, GlobeIcon, ServerIcon, HourglassIcon } from "lucide-react"
|
import { BellIcon, GlobeIcon, ServerIcon } from "lucide-react"
|
||||||
import { alertInfo, cn } from "@/lib/utils"
|
import { alertInfo, cn } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { AlertRecord, SystemRecord } from "@/types"
|
import { AlertRecord, SystemRecord } from "@/types"
|
||||||
import { $router, Link } from "../router"
|
import { $router, Link } from "../router"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { Checkbox } from "../ui/checkbox"
|
import { Checkbox } from "../ui/checkbox"
|
||||||
import { Collapsible } from "../ui/collapsible"
|
|
||||||
import { SystemAlert, SystemAlertGlobal } from "./alerts-system"
|
import { SystemAlert, SystemAlertGlobal } from "./alerts-system"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||||
import { memo, useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
import { useYAxisWidth, cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
chartMargin,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
|
formatBytes,
|
||||||
|
decimalString,
|
||||||
|
toFixedFloat,
|
||||||
|
} 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, Unit } from "@/lib/enums"
|
import { ChartType, DataUnit } from "@/lib/enums"
|
||||||
|
|
||||||
export default memo(function ContainerChart({
|
export default memo(function ContainerChart({
|
||||||
dataKey,
|
dataKey,
|
||||||
@@ -76,11 +85,11 @@ export default memo(function ContainerChart({
|
|||||||
// tick formatter
|
// tick formatter
|
||||||
if (chartType === ChartType.CPU) {
|
if (chartType === ChartType.CPU) {
|
||||||
obj.tickFormatter = (value) => {
|
obj.tickFormatter = (value) => {
|
||||||
const val = toFixedFloat(value, 2) + unit
|
const val = toFixedWithoutTrailingZeros(value, 2) + unit
|
||||||
return updateYAxisWidth(val)
|
return updateYAxisWidth(val)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes
|
const chartUnit = isNetChart ? userSettings.unitNet : DataUnit.Bytes
|
||||||
obj.tickFormatter = (val) => {
|
obj.tickFormatter = (val) => {
|
||||||
const { value, unit } = formatBytes(val, isNetChart, chartUnit, true)
|
const { value, unit } = formatBytes(val, isNetChart, chartUnit, true)
|
||||||
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
||||||
@@ -109,7 +118,7 @@ export default memo(function ContainerChart({
|
|||||||
}
|
}
|
||||||
} else if (chartType === ChartType.Memory) {
|
} else if (chartType === ChartType.Memory) {
|
||||||
obj.toolTipFormatter = (item: any) => {
|
obj.toolTipFormatter = (item: any) => {
|
||||||
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
|
const { value, unit } = formatBytes(item.value, false, DataUnit.Bytes, true)
|
||||||
return decimalString(value) + " " + unit
|
return decimalString(value) + " " + unit
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,10 +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, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
import { useYAxisWidth, cn, formatShortDate, decimalString, toFixedFloat, chartMargin, formatBytes } 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"
|
import { DataUnit } from "@/lib/enums"
|
||||||
|
|
||||||
export default memo(function DiskChart({
|
export default memo(function DiskChart({
|
||||||
dataKey,
|
dataKey,
|
||||||
@@ -47,7 +47,7 @@ export default memo(function DiskChart({
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(val) => {
|
tickFormatter={(val) => {
|
||||||
const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
|
const { value, unit } = formatBytes(val * 1024, false, DataUnit.Bytes, true)
|
||||||
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -59,7 +59,7 @@ 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 { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
const { value: convertedValue, unit } = formatBytes(value * 1024, false, DataUnit.Bytes, true)
|
||||||
return decimalString(convertedValue) + " " + unit
|
return decimalString(convertedValue) + " " + unit
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
xAxis,
|
xAxis,
|
||||||
} from "@/components/ui/chart"
|
} from "@/components/ui/chart"
|
||||||
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
|
decimalString,
|
||||||
|
chartMargin,
|
||||||
|
} from "@/lib/utils"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo, useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
|
|
||||||
@@ -65,7 +72,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
|||||||
domain={[0, "auto"]}
|
domain={[0, "auto"]}
|
||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
tickFormatter={(value) => {
|
tickFormatter={(value) => {
|
||||||
const val = toFixedFloat(value, 2)
|
const val = toFixedWithoutTrailingZeros(value, 2)
|
||||||
return updateYAxisWidth(val + "W")
|
return updateYAxisWidth(val + "W")
|
||||||
}}
|
}}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
|
|
||||||
|
|
||||||
import {
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
xAxis,
|
|
||||||
} from "@/components/ui/chart"
|
|
||||||
import {
|
|
||||||
useYAxisWidth,
|
|
||||||
cn,
|
|
||||||
formatShortDate,
|
|
||||||
toFixedWithoutTrailingZeros,
|
|
||||||
decimalString,
|
|
||||||
chartMargin,
|
|
||||||
} from "@/lib/utils"
|
|
||||||
import { ChartData } from "@/types"
|
|
||||||
import { memo, useMemo } from "react"
|
|
||||||
import { t } from "@lingui/core/macro"
|
|
||||||
|
|
||||||
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
|
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
|
||||||
|
|
||||||
if (chartData.systemStats.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format load average data for chart */
|
|
||||||
const newChartData = useMemo(() => {
|
|
||||||
const newChartData = { data: [], colors: {} } as {
|
|
||||||
data: Record<string, number | string>[]
|
|
||||||
colors: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define colors for the three load average lines
|
|
||||||
const colors = {
|
|
||||||
"1m": "hsl(25, 95%, 53%)", // Orange for 1-minute
|
|
||||||
"5m": "hsl(217, 91%, 60%)", // Blue for 5-minute
|
|
||||||
"15m": "hsl(271, 81%, 56%)", // Purple for 15-minute
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let data of chartData.systemStats) {
|
|
||||||
let newData = { created: data.created } as Record<string, number | string>
|
|
||||||
|
|
||||||
// Add load average values if they exist and stats is not null
|
|
||||||
if (data.stats && data.stats.l1 !== undefined) {
|
|
||||||
newData["1m"] = data.stats.l1
|
|
||||||
}
|
|
||||||
if (data.stats && data.stats.l5 !== undefined) {
|
|
||||||
newData["5m"] = data.stats.l5
|
|
||||||
}
|
|
||||||
if (data.stats && data.stats.l15 !== undefined) {
|
|
||||||
newData["15m"] = data.stats.l15
|
|
||||||
}
|
|
||||||
|
|
||||||
newChartData.data.push(newData)
|
|
||||||
}
|
|
||||||
|
|
||||||
newChartData.colors = colors
|
|
||||||
return newChartData
|
|
||||||
}, [chartData])
|
|
||||||
|
|
||||||
const loadKeys = ["1m", "5m", "15m"].filter(key =>
|
|
||||||
newChartData.data.some(data => data[key] !== undefined)
|
|
||||||
)
|
|
||||||
|
|
||||||
// console.log('rendered at', new Date())
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ChartContainer
|
|
||||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
|
||||||
"opacity-100": yAxisWidth,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<YAxis
|
|
||||||
direction="ltr"
|
|
||||||
orientation={chartData.orientation}
|
|
||||||
className="tracking-tighter"
|
|
||||||
domain={[0, "auto"]}
|
|
||||||
width={yAxisWidth}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
const val = toFixedWithoutTrailingZeros(value, 2)
|
|
||||||
return updateYAxisWidth(val)
|
|
||||||
}}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
/>
|
|
||||||
{xAxis(chartData)}
|
|
||||||
<ChartTooltip
|
|
||||||
animationEasing="ease-out"
|
|
||||||
animationDuration={150}
|
|
||||||
// @ts-ignore
|
|
||||||
itemSorter={(a, b) => b.value - a.value}
|
|
||||||
content={
|
|
||||||
<ChartTooltipContent
|
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
|
||||||
contentFormatter={(item) => decimalString(item.value)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{loadKeys.map((key) => (
|
|
||||||
<Line
|
|
||||||
key={key}
|
|
||||||
dataKey={key}
|
|
||||||
name={key === "1m" ? t`1 min` : key === "5m" ? t`5 min` : t`15 min`}
|
|
||||||
type="monotoneX"
|
|
||||||
dot={false}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke={newChartData.colors[key]}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
|
||||||
</LineChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -1,10 +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, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } 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"
|
import { DataUnit } 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()
|
||||||
@@ -40,7 +40,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => {
|
tickFormatter={(value) => {
|
||||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
const { value: convertedValue, unit } = formatBytes(value * 1024, false, DataUnit.Bytes, true)
|
||||||
return updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
|
return updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -57,7 +57,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
|||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
contentFormatter={({ value }) => {
|
contentFormatter={({ value }) => {
|
||||||
// mem values are supplied as GB
|
// mem values are supplied as GB
|
||||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
const { value: convertedValue, unit } = formatBytes(value * 1024, false, DataUnit.Bytes, true)
|
||||||
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
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"
|
||||||
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
import {
|
||||||
|
useYAxisWidth,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
|
decimalString,
|
||||||
|
chartMargin,
|
||||||
|
} from "@/lib/utils"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { $userSettings } from "@/lib/stores"
|
|
||||||
import { useStore } from "@nanostores/react"
|
|
||||||
|
|
||||||
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 = useStore($userSettings)
|
|
||||||
|
|
||||||
if (chartData.systemStats.length === 0) {
|
if (chartData.systemStats.length === 0) {
|
||||||
return null
|
return null
|
||||||
@@ -29,14 +33,11 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
|
|||||||
direction="ltr"
|
direction="ltr"
|
||||||
orientation={chartData.orientation}
|
orientation={chartData.orientation}
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
domain={[0, () => toFixedFloat(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}
|
domain={[0, () => toFixedWithoutTrailingZeros(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}
|
||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => {
|
tickFormatter={(value) => updateYAxisWidth(value + " GB")}
|
||||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true)
|
|
||||||
return updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{xAxis(chartData)}
|
{xAxis(chartData)}
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
@@ -45,11 +46,7 @@ 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={({ value }) => {
|
contentFormatter={(item) => decimalString(item.value) + " GB"}
|
||||||
// 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"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import {
|
|||||||
useYAxisWidth,
|
useYAxisWidth,
|
||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedFloat,
|
toFixedWithoutTrailingZeros,
|
||||||
chartMargin,
|
chartMargin,
|
||||||
formatTemperature,
|
convertTemperature,
|
||||||
decimalString,
|
decimalString,
|
||||||
} from "@/lib/utils"
|
} from "@/lib/utils"
|
||||||
import { ChartData } from "@/types"
|
import { ChartData } from "@/types"
|
||||||
@@ -75,8 +75,8 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
|||||||
domain={[0, "auto"]}
|
domain={[0, "auto"]}
|
||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
tickFormatter={(val) => {
|
tickFormatter={(val) => {
|
||||||
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
|
const { value, unit } = convertTemperature(val, userSettings.unitTemp)
|
||||||
return updateYAxisWidth(toFixedFloat(value, 2) + " " + unit)
|
return updateYAxisWidth(toFixedWithoutTrailingZeros(value, 2) + " " + unit)
|
||||||
}}
|
}}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
@@ -91,7 +91,7 @@ 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 { value, unit } = formatTemperature(item.value, userSettings.unitTemp)
|
const { value, unit } = convertTemperature(item.value, userSettings.unitTemp)
|
||||||
return decimalString(value) + " " + unit
|
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 { Unit } from "@/lib/enums"
|
import { DataUnit, TemperatureUnit } 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)
|
||||||
@@ -118,41 +118,33 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
<Select
|
<Select
|
||||||
name="unitTemp"
|
name="unitTemp"
|
||||||
key={userSettings.unitTemp}
|
key={userSettings.unitTemp}
|
||||||
defaultValue={userSettings.unitTemp?.toString() || String(Unit.Celsius)}
|
defaultValue={userSettings.unitTemp?.toString() || String(TemperatureUnit.Celsius)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="unitTemp">
|
<SelectTrigger id="unitTemp">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={String(Unit.Celsius)}>
|
<SelectItem value={String(TemperatureUnit.Celsius)}>Celsius (°C)</SelectItem>
|
||||||
<Trans>Celsius (°C)</Trans>
|
<SelectItem value={String(TemperatureUnit.Fahrenheit)}>Fahrenheit (°F)</SelectItem>
|
||||||
</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="unitNet">
|
<Label className="block" htmlFor="unitTemp">
|
||||||
<Trans>Network unit</Trans>
|
<Trans>Network unit</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
name="unitNet"
|
name="unitNet"
|
||||||
key={userSettings.unitNet}
|
key={userSettings.unitNet}
|
||||||
defaultValue={userSettings.unitNet?.toString() ?? String(Unit.Bytes)}
|
defaultValue={userSettings.unitNet?.toString() ?? String(DataUnit.Bytes)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="unitNet">
|
<SelectTrigger id="unitTemp">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={String(Unit.Bytes)}>
|
<SelectItem value={String(DataUnit.Bytes)}>Bytes (KB/s, MB/s, GB/s)</SelectItem>
|
||||||
<Trans>Bytes (KB/s, MB/s, GB/s)</Trans>
|
<SelectItem value={String(DataUnit.Bits)}>Bits (kbps, Mbps, Gbps)</SelectItem>
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={String(Unit.Bits)}>
|
|
||||||
<Trans>Bits (Kbps, Mbps, Gbps)</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,18 +156,14 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
<Select
|
<Select
|
||||||
name="unitDisk"
|
name="unitDisk"
|
||||||
key={userSettings.unitDisk}
|
key={userSettings.unitDisk}
|
||||||
defaultValue={userSettings.unitDisk?.toString() ?? String(Unit.Bytes)}
|
defaultValue={userSettings.unitDisk?.toString() ?? String(DataUnit.Bytes)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="unitDisk">
|
<SelectTrigger id="unitDisk">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={String(Unit.Bytes)}>
|
<SelectItem value={String(DataUnit.Bytes)}>Bytes (KB/s, MB/s, GB/s)</SelectItem>
|
||||||
<Trans>Bytes (KB/s, MB/s, GB/s)</Trans>
|
<SelectItem value={String(DataUnit.Bits)}>Bits (kbps, Mbps, Gbps)</SelectItem>
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={String(Unit.Bits)}>
|
|
||||||
<Trans>Bits (Kbps, Mbps, Gbps)</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default function SettingsLayout() {
|
|||||||
title: t`Tokens & Fingerprints`,
|
title: t`Tokens & Fingerprints`,
|
||||||
href: getPagePath($router, "settings", { name: "tokens" }),
|
href: getPagePath($router, "settings", { name: "tokens" }),
|
||||||
icon: FingerprintIcon,
|
icon: FingerprintIcon,
|
||||||
noReadOnly: true,
|
// admin: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t`YAML Config`,
|
title: t`YAML Config`,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { cn, isAdmin, isReadOnlyUser } from "@/lib/utils"
|
import { cn, isAdmin } from "@/lib/utils"
|
||||||
import { buttonVariants } from "../../ui/button"
|
import { buttonVariants } from "../../ui/button"
|
||||||
import { $router, Link, navigate } from "../../router"
|
import { $router, Link, navigate } from "../../router"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
@@ -12,7 +12,6 @@ interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
|||||||
title: string
|
title: string
|
||||||
icon?: React.FC<React.SVGProps<SVGSVGElement>>
|
icon?: React.FC<React.SVGProps<SVGSVGElement>>
|
||||||
admin?: boolean
|
admin?: boolean
|
||||||
noReadOnly?: boolean
|
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
|||||||
{/* Desktop View */}
|
{/* Desktop View */}
|
||||||
<nav className={cn("hidden md:grid gap-1", className)} {...props}>
|
<nav className={cn("hidden md:grid gap-1", className)} {...props}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
if ((item.admin && !isAdmin()) || (item.noReadOnly && isReadOnlyUser())) {
|
if (item.admin && !isAdmin()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ import {
|
|||||||
InstallDropdown,
|
InstallDropdown,
|
||||||
} from "@/components/install-dropdowns"
|
} from "@/components/install-dropdowns"
|
||||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons"
|
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons"
|
||||||
import { redirectPage } from "@nanostores/router"
|
|
||||||
import { $router } from "@/components/router"
|
|
||||||
|
|
||||||
const pbFingerprintOptions = {
|
const pbFingerprintOptions = {
|
||||||
expand: "system",
|
expand: "system",
|
||||||
@@ -43,9 +41,6 @@ const pbFingerprintOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SettingsFingerprintsPage = memo(() => {
|
const SettingsFingerprintsPage = memo(() => {
|
||||||
if (isReadOnlyUser()) {
|
|
||||||
redirectPage($router, "settings", { name: "general" })
|
|
||||||
}
|
|
||||||
const [fingerprints, setFingerprints] = useState<FingerprintRecord[]>([])
|
const [fingerprints, setFingerprints] = useState<FingerprintRecord[]>([])
|
||||||
|
|
||||||
// Get fingerprint records on mount
|
// Get fingerprint records on mount
|
||||||
|
|||||||
@@ -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, Unit, Os } from "@/lib/enums"
|
import { ChartType, DataUnit, 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"
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
getPbTimestamp,
|
getPbTimestamp,
|
||||||
listen,
|
listen,
|
||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
useLocalStorage,
|
useLocalStorage,
|
||||||
} from "@/lib/utils"
|
} from "@/lib/utils"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
@@ -48,7 +49,6 @@ const DiskChart = lazy(() => import("../charts/disk-chart"))
|
|||||||
const SwapChart = lazy(() => import("../charts/swap-chart"))
|
const SwapChart = lazy(() => import("../charts/swap-chart"))
|
||||||
const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
|
const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
|
||||||
const GpuPowerChart = lazy(() => import("../charts/gpu-power-chart"))
|
const GpuPowerChart = lazy(() => import("../charts/gpu-power-chart"))
|
||||||
const LoadAverageChart = lazy(() => import("../charts/load-average-chart"))
|
|
||||||
|
|
||||||
const cache = new Map<string, any>()
|
const cache = new Map<string, any>()
|
||||||
|
|
||||||
@@ -479,23 +479,11 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
chartName="CPU Usage"
|
chartName="CPU Usage"
|
||||||
maxToggled={maxValues}
|
maxToggled={maxValues}
|
||||||
tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
|
tickFormatter={(val) => toFixedWithoutTrailingZeros(val, 2) + "%"}
|
||||||
contentFormatter={({ value }) => decimalString(value) + "%"}
|
contentFormatter={({ value }) => decimalString(value) + "%"}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
{/* Load Average chart */}
|
|
||||||
{(systemStats.at(-1)?.stats.l1 !== undefined || systemStats.at(-1)?.stats.l5 !== undefined || systemStats.at(-1)?.stats.l15 !== undefined) && (
|
|
||||||
<ChartCard
|
|
||||||
empty={dataEmpty}
|
|
||||||
grid={grid}
|
|
||||||
title={t`Load Average`}
|
|
||||||
description={t`System load averages over time`}
|
|
||||||
>
|
|
||||||
<LoadAverageChart chartData={chartData} />
|
|
||||||
</ChartCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{containerFilterBar && (
|
{containerFilterBar && (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
empty={dataEmpty}
|
empty={dataEmpty}
|
||||||
@@ -568,6 +556,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
maxToggled={maxValues}
|
maxToggled={maxValues}
|
||||||
tickFormatter={(val) => {
|
tickFormatter={(val) => {
|
||||||
let { value, unit } = formatBytes(val, true, userSettings.unitNet, true)
|
let { value, unit } = formatBytes(val, true, userSettings.unitNet, true)
|
||||||
|
// value = value >= 10 ? Math.ceil(value) : value
|
||||||
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||||
}}
|
}}
|
||||||
contentFormatter={({ value }) => {
|
contentFormatter={({ value }) => {
|
||||||
@@ -639,6 +628,10 @@ 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
|
||||||
@@ -650,7 +643,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
<AreaChartDefault
|
<AreaChartDefault
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
chartName={`g.${id}.u`}
|
chartName={`g.${id}.u`}
|
||||||
tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
|
tickFormatter={(val) => toFixedWithoutTrailingZeros(val, 2) + "%"}
|
||||||
contentFormatter={({ value }) => decimalString(value) + "%"}
|
contentFormatter={({ value }) => decimalString(value) + "%"}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
@@ -665,11 +658,11 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
chartName={`g.${id}.mu`}
|
chartName={`g.${id}.mu`}
|
||||||
max={gpu.mt}
|
max={gpu.mt}
|
||||||
tickFormatter={(val) => {
|
tickFormatter={(val) => {
|
||||||
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
|
const { value, unit } = formatBytes(val, false, DataUnit.Bytes, true)
|
||||||
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||||
}}
|
}}
|
||||||
contentFormatter={({ value }) => {
|
contentFormatter={({ value }) => {
|
||||||
const { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true)
|
const { value: convertedValue, unit } = formatBytes(value, false, DataUnit.Bytes, true)
|
||||||
return decimalString(convertedValue) + " " + unit
|
return decimalString(convertedValue) + " " + unit
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import {
|
|||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
isReadOnlyUser,
|
isReadOnlyUser,
|
||||||
useLocalStorage,
|
useLocalStorage,
|
||||||
formatTemperature,
|
convertTemperature,
|
||||||
decimalString,
|
decimalString,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
} from "@/lib/utils"
|
} from "@/lib/utils"
|
||||||
@@ -84,7 +84,6 @@ import { ClassValue } from "clsx"
|
|||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import { SystemDialog } from "../add-system"
|
import { SystemDialog } from "../add-system"
|
||||||
import { Dialog } from "../ui/dialog"
|
import { Dialog } from "../ui/dialog"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
|
||||||
|
|
||||||
type ViewMode = "table" | "grid"
|
type ViewMode = "table" | "grid"
|
||||||
|
|
||||||
@@ -136,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
|
||||||
|
|
||||||
@@ -230,7 +230,6 @@ export default function SystemsTable() {
|
|||||||
Icon: EthernetIcon,
|
Icon: EthernetIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
cell(info) {
|
cell(info) {
|
||||||
const userSettings = useStore($userSettings)
|
|
||||||
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, true)
|
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, true)
|
||||||
return (
|
return (
|
||||||
<span className="tabular-nums whitespace-nowrap">
|
<span className="tabular-nums whitespace-nowrap">
|
||||||
@@ -240,61 +239,43 @@ export default function SystemsTable() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "loadAverage",
|
accessorFn: (originalRow) => originalRow.info.l5,
|
||||||
name: () => t`Load Average`,
|
id: "l5",
|
||||||
|
name: () => t({ message: "L5", comment: "Load average 5 minutes" }),
|
||||||
size: 0,
|
size: 0,
|
||||||
hideSort: true,
|
hideSort: true,
|
||||||
Icon: HourglassIcon,
|
Icon: HourglassIcon,
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
cell(info: CellContext<SystemRecord, unknown>) {
|
cell(info) {
|
||||||
const system = info.row.original;
|
const val = info.getValue() as number
|
||||||
const l1 = system.info?.l1;
|
if (!val) {
|
||||||
const l5 = system.info?.l5;
|
return null
|
||||||
const l15 = system.info?.l15;
|
|
||||||
const cores = system.info?.c || 1;
|
|
||||||
|
|
||||||
// If no load average data, return null
|
|
||||||
if (!l1 && !l5 && !l15) return null;
|
|
||||||
|
|
||||||
const loadAverages = [
|
|
||||||
{ name: "1m", value: l1 },
|
|
||||||
{ name: "5m", value: l5 },
|
|
||||||
{ name: "15m", value: l15 }
|
|
||||||
].filter(la => la.value !== undefined);
|
|
||||||
|
|
||||||
if (!loadAverages.length) return null;
|
|
||||||
|
|
||||||
function getDotColor(value: number) {
|
|
||||||
const normalized = value / cores;
|
|
||||||
if (normalized < 0.7) return "bg-green-500";
|
|
||||||
if (normalized < 1.0) return "bg-orange-500";
|
|
||||||
return "bg-red-600";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 w-full">
|
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-1")}>
|
||||||
{loadAverages.map((la, idx) => (
|
{decimalString(val)}
|
||||||
<TooltipProvider key={la.name}>
|
</span>
|
||||||
<Tooltip>
|
)
|
||||||
<TooltipTrigger asChild>
|
},
|
||||||
<span className="flex items-center cursor-pointer">
|
},
|
||||||
<span className={cn("inline-block w-2 h-2 rounded-full mr-1", getDotColor(la.value || 0))} />
|
{
|
||||||
<span className="tabular-nums">
|
accessorFn: (originalRow) => originalRow.info.l15,
|
||||||
{decimalString(la.value || 0, 2)}
|
id: "l15",
|
||||||
</span>
|
name: () => t({ message: "L15", comment: "Load average 15 minutes" }),
|
||||||
{idx < loadAverages.length - 1 && <span className="mx-1 text-muted-foreground">/</span>}
|
size: 0,
|
||||||
</span>
|
hideSort: true,
|
||||||
</TooltipTrigger>
|
Icon: HourglassIcon,
|
||||||
<TooltipContent side="top">
|
header: sortableHeader,
|
||||||
<div className="text-center">
|
cell(info) {
|
||||||
<div className="font-medium">{t`${la.name}`}</div>
|
const val = info.getValue() as number
|
||||||
</div>
|
if (!val) {
|
||||||
</TooltipContent>
|
return null
|
||||||
</Tooltip>
|
}
|
||||||
</TooltipProvider>
|
return (
|
||||||
))}
|
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-1")}>
|
||||||
</div>
|
{decimalString(val)}
|
||||||
);
|
</span>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -310,8 +291,7 @@ export default function SystemsTable() {
|
|||||||
if (!val) {
|
if (!val) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const userSettings = useStore($userSettings)
|
const { value, unit } = convertTemperature(val, userSettings.unitTemp)
|
||||||
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
|
|
||||||
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)} {unit}
|
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { ChevronDownIcon, HourglassIcon } from "lucide-react"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "./button"
|
|
||||||
|
|
||||||
interface CollapsibleProps {
|
|
||||||
title: string
|
|
||||||
children: React.ReactNode
|
|
||||||
description?: React.ReactNode
|
|
||||||
defaultOpen?: boolean
|
|
||||||
className?: string
|
|
||||||
icon?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Collapsible({ title, children, description, defaultOpen = false, className, icon }: CollapsibleProps) {
|
|
||||||
const [isOpen, setIsOpen] = React.useState(defaultOpen)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("border rounded-lg", className)}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full justify-between p-4 font-semibold"
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{icon}
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={cn("h-4 w-4 transition-transform duration-200", {
|
|
||||||
"rotate-180": isOpen,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
{description && (
|
|
||||||
<div className="px-4 pb-2 text-sm text-muted-foreground">
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isOpen && (
|
|
||||||
<div className="px-4 pb-4">
|
|
||||||
<div className="grid gap-3">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
/** Operating system */
|
|
||||||
export enum Os {
|
export enum Os {
|
||||||
Linux = 0,
|
Linux = 0,
|
||||||
Darwin,
|
Darwin,
|
||||||
@@ -6,7 +5,6 @@ export enum Os {
|
|||||||
FreeBSD,
|
FreeBSD,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Type of chart */
|
|
||||||
export enum ChartType {
|
export enum ChartType {
|
||||||
Memory,
|
Memory,
|
||||||
Disk,
|
Disk,
|
||||||
@@ -14,10 +12,12 @@ export enum ChartType {
|
|||||||
CPU,
|
CPU,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unit of measurement */
|
export enum DataUnit {
|
||||||
export enum Unit {
|
|
||||||
Bytes,
|
Bytes,
|
||||||
Bits,
|
Bits,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TemperatureUnit {
|
||||||
Celsius,
|
Celsius,
|
||||||
Fahrenheit,
|
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 || ""],
|
||||||
// unitTemp: "celsius",
|
temperatureUnit: "celsius",
|
||||||
// unitNet: "mbps",
|
networkUnit: "mbps",
|
||||||
// unitDisk: "mbps",
|
diskUnit: "mbps",
|
||||||
})
|
})
|
||||||
// update local storage on change
|
// update local storage on change
|
||||||
$userSettings.subscribe((value) => {
|
$userSettings.subscribe((value) => {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
ChartTimes,
|
ChartTimes,
|
||||||
FingerprintRecord,
|
FingerprintRecord,
|
||||||
SystemRecord,
|
SystemRecord,
|
||||||
UserSettings,
|
TemperatureConversion,
|
||||||
|
DataUnitConversion,
|
||||||
} from "@/types"
|
} from "@/types"
|
||||||
import { RecordModel, RecordSubscription } from "pocketbase"
|
import { RecordModel, RecordSubscription } from "pocketbase"
|
||||||
import { WritableAtom } from "nanostores"
|
import { WritableAtom } from "nanostores"
|
||||||
@@ -19,7 +20,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"
|
import { DataUnit, TemperatureUnit } from "./enums"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
@@ -82,10 +83,7 @@ export const updateSystemList = (() => {
|
|||||||
|
|
||||||
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
||||||
export async function logOut() {
|
export async function logOut() {
|
||||||
$systems.set([])
|
sessionStorage.setItem("lo", "t")
|
||||||
$alerts.set([])
|
|
||||||
$userSettings.set({} as UserSettings)
|
|
||||||
sessionStorage.setItem("lo", "t") // prevent auto login on logout
|
|
||||||
pb.authStore.clear()
|
pb.authStore.clear()
|
||||||
pb.realtime.unsubscribe()
|
pb.realtime.unsubscribe()
|
||||||
}
|
}
|
||||||
@@ -237,17 +235,17 @@ export function useYAxisWidth() {
|
|||||||
return { yAxisWidth, updateYAxisWidth }
|
return { yAxisWidth, updateYAxisWidth }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format number to x decimal places, without trailing zeros */
|
export function toFixedWithoutTrailingZeros(num: number, digits: number) {
|
||||||
|
return parseFloat(num.toFixed(digits)).toString()
|
||||||
|
}
|
||||||
|
|
||||||
export function toFixedFloat(num: number, digits: number) {
|
export function toFixedFloat(num: number, digits: number) {
|
||||||
return parseFloat((digits === 0 ? Math.ceil(num) : 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, maintaining trailing zeros */
|
/** 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, {
|
||||||
@@ -278,13 +276,11 @@ export function useLocalStorage<T>(key: string, defaultValue: T) {
|
|||||||
return [value, setValue]
|
return [value, setValue]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format temperature to user's preferred unit */
|
export function convertTemperature(celsius: number, unit = TemperatureUnit.Celsius): TemperatureConversion {
|
||||||
export function formatTemperature(celsius: number, unit?: Unit): { value: number; unit: string } {
|
const userSettings = $userSettings.get()
|
||||||
if (!unit) {
|
unit ||= userSettings.unitTemp || TemperatureUnit.Celsius
|
||||||
unit = $userSettings.get().unitTemp || Unit.Celsius
|
|
||||||
}
|
|
||||||
// need loose equality check due to form data being strings
|
// need loose equality check due to form data being strings
|
||||||
if (unit == Unit.Fahrenheit) {
|
if (unit == TemperatureUnit.Fahrenheit) {
|
||||||
return {
|
return {
|
||||||
value: celsius * 1.8 + 32,
|
value: celsius * 1.8 + 32,
|
||||||
unit: "°F",
|
unit: "°F",
|
||||||
@@ -296,18 +292,17 @@ export function formatTemperature(celsius: number, unit?: Unit): { value: number
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format bytes to user's preferred unit */
|
|
||||||
export function formatBytes(
|
export function formatBytes(
|
||||||
size: number,
|
size: number,
|
||||||
perSecond = false,
|
perSecond = false,
|
||||||
unit = Unit.Bytes,
|
unit = DataUnit.Bytes,
|
||||||
isMegabytes = false
|
isMegabytes = false
|
||||||
): { value: number; unit: string } {
|
): DataUnitConversion {
|
||||||
// Convert MB to bytes if isMegabytes is true
|
// Convert MB to bytes if isMegabytes is true
|
||||||
if (isMegabytes) size *= 1024 * 1024
|
if (isMegabytes) size *= 1024 * 1024
|
||||||
|
|
||||||
// need loose equality check due to form data being strings
|
// need loose equality check due to form data being strings
|
||||||
if (unit == Unit.Bits) {
|
if (unit == DataUnit.Bits) {
|
||||||
const bits = size * 8
|
const bits = size * 8
|
||||||
const suffix = perSecond ? "ps" : ""
|
const suffix = perSecond ? "ps" : ""
|
||||||
if (bits < 1000) return { value: bits, unit: `b${suffix}` }
|
if (bits < 1000) return { value: bits, unit: `b${suffix}` }
|
||||||
@@ -327,7 +322,7 @@ export function formatBytes(
|
|||||||
unit: `Tb${suffix}`,
|
unit: `Tb${suffix}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// bytes
|
|
||||||
const suffix = perSecond ? "/s" : ""
|
const suffix = perSecond ? "/s" : ""
|
||||||
if (size < 100) return { value: size, unit: `B${suffix}` }
|
if (size < 100) return { value: size, unit: `B${suffix}` }
|
||||||
if (size < 1000 * 1024) return { value: size / 1024, unit: `KB${suffix}` }
|
if (size < 1000 * 1024) return { value: size / 1024, unit: `KB${suffix}` }
|
||||||
@@ -347,24 +342,40 @@ export function formatBytes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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.error("get settings", e)
|
console.log("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.error("create settings", e)
|
console.log("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> = {
|
||||||
@@ -407,16 +418,6 @@ export const alertInfo: Record<string, AlertInfo> = {
|
|||||||
icon: ThermometerIcon,
|
icon: ThermometerIcon,
|
||||||
desc: () => t`Triggers when any sensor exceeds a threshold`,
|
desc: () => t`Triggers when any sensor exceeds a threshold`,
|
||||||
},
|
},
|
||||||
LoadAvg1: {
|
|
||||||
name: () => t`Load Average 1m`,
|
|
||||||
unit: "",
|
|
||||||
icon: HourglassIcon,
|
|
||||||
max: 100,
|
|
||||||
min: 0.1,
|
|
||||||
start: 10,
|
|
||||||
step: 0.1,
|
|
||||||
desc: () => t`Triggers when 1 minute load average exceeds a threshold`,
|
|
||||||
},
|
|
||||||
LoadAvg5: {
|
LoadAvg5: {
|
||||||
name: () => t`Load Average 5m`,
|
name: () => t`Load Average 5m`,
|
||||||
unit: "",
|
unit: "",
|
||||||
@@ -455,27 +456,3 @@ export const getHubURL = () => BESZEL?.HUB_URL || window.location.origin
|
|||||||
|
|
||||||
/** Map of system IDs to their corresponding tokens (used to avoid fetching in add-system dialog) */
|
/** Map of system IDs to their corresponding tokens (used to avoid fetching in add-system dialog) */
|
||||||
export const tokenMap = new Map<SystemRecord["id"], FingerprintRecord["token"]>()
|
export const tokenMap = new Map<SystemRecord["id"], FingerprintRecord["token"]>()
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate load average percentage relative to CPU cores
|
|
||||||
* @param loadAverage - The load average value (1m, 5m, or 15m)
|
|
||||||
* @param cores - Number of CPU cores
|
|
||||||
* @returns Percentage (0-100) representing CPU utilization
|
|
||||||
*/
|
|
||||||
export const calculateLoadAveragePercent = (loadAverage: number, cores: number): number => {
|
|
||||||
if (!loadAverage || !cores) return 0
|
|
||||||
return Math.min((loadAverage / cores) * 100, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get load average opacity based on utilization relative to cores
|
|
||||||
* @param loadAverage - The load average value
|
|
||||||
* @param cores - Number of CPU cores
|
|
||||||
* @returns Opacity value (0.6, 0.8, or 1.0)
|
|
||||||
*/
|
|
||||||
export const getLoadAverageOpacity = (loadAverage: number, cores: number): number => {
|
|
||||||
if (!loadAverage || !cores) return 0.6
|
|
||||||
if (loadAverage < cores * 0.5) return 0.6
|
|
||||||
if (loadAverage < cores) return 0.8
|
|
||||||
return 1.0
|
|
||||||
}
|
|
||||||
21
beszel/site/src/types.d.ts
vendored
21
beszel/site/src/types.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
import { RecordModel } from "pocketbase"
|
import { RecordModel } from "pocketbase"
|
||||||
import { Unit, Os } from "./lib/enums"
|
import { DataUnit, Os, TemperatureUnit } from "./lib/enums"
|
||||||
|
|
||||||
// global window properties
|
// global window properties
|
||||||
declare global {
|
declare global {
|
||||||
@@ -22,6 +22,17 @@ export interface FingerprintRecord extends RecordModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unit conversion result types
|
||||||
|
export interface TemperatureConversion {
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataUnitConversion {
|
||||||
|
value: number
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SystemRecord extends RecordModel {
|
export interface SystemRecord extends RecordModel {
|
||||||
name: string
|
name: string
|
||||||
host: string
|
host: string
|
||||||
@@ -44,8 +55,6 @@ export interface SystemInfo {
|
|||||||
c: number
|
c: number
|
||||||
/** cpu model */
|
/** cpu model */
|
||||||
m: string
|
m: string
|
||||||
/** load average 1 minute */
|
|
||||||
l1?: number
|
|
||||||
/** load average 5 minutes */
|
/** load average 5 minutes */
|
||||||
l5?: number
|
l5?: number
|
||||||
/** load average 15 minutes */
|
/** load average 15 minutes */
|
||||||
@@ -207,9 +216,9 @@ export type UserSettings = {
|
|||||||
chartTime: ChartTimes
|
chartTime: ChartTimes
|
||||||
emails?: string[]
|
emails?: string[]
|
||||||
webhooks?: string[]
|
webhooks?: string[]
|
||||||
unitTemp?: Unit
|
unitTemp?: TemperatureUnit
|
||||||
unitNet?: Unit
|
unitNet?: DataUnit
|
||||||
unitDisk?: Unit
|
unitDisk?: DataUnit
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartDataContainer = {
|
type ChartDataContainer = {
|
||||||
|
|||||||
Reference in New Issue
Block a user