hub(ui): tabs display for system + major frontend/charts refactoring

- System page tabs display option
- Remove very specific chart components (disk usage, container cpu, etc)
and refactor to use more flexible area and line chart components
- Optimizations around chart handling to decrease mem usage. Charts are
only redrawn now if in view.
- Resolve most of the react dev warnings

Co-authored-by: sveng93 <svenvanginkel@icloud.com>
This commit is contained in:
henrygd
2026-03-26 15:21:39 -04:00
parent 35528332fd
commit 62587919f4
34 changed files with 2368 additions and 2019 deletions

View File

@@ -0,0 +1,160 @@
import { t } from "@lingui/core/macro"
import AreaChartDefault from "@/components/charts/area-chart"
import { batteryStateTranslations } from "@/lib/i18n"
import { $temperatureFilter, $userSettings } from "@/lib/stores"
import { cn, decimalString, formatTemperature, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import { ChartCard, FilterBar } from "../chart-card"
import LineChartDefault from "@/components/charts/line-chart"
import { useStore } from "@nanostores/react"
import { useRef, useMemo } from "react"
export function BatteryChart({
chartData,
grid,
dataEmpty,
maxValues,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
maxValues: boolean
}) {
const showBatteryChart = chartData.systemStats.at(-1)?.stats.bat
if (!showBatteryChart) {
return null
}
return (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Battery`}
description={`${t({
message: "Current state",
comment: "Context: Battery state",
})}: ${batteryStateTranslations[chartData.systemStats.at(-1)?.stats.bat?.[1] ?? 0]()}`}
>
<AreaChartDefault
chartData={chartData}
maxToggled={maxValues}
dataPoints={[
{
label: t`Charge`,
dataKey: ({ stats }) => stats?.bat?.[0],
color: 1,
opacity: 0.35,
},
]}
domain={[0, 100]}
tickFormatter={(val) => `${val}%`}
contentFormatter={({ value }) => `${value}%`}
/>
</ChartCard>
)
}
export function TemperatureChart({
chartData,
grid,
dataEmpty,
}: {
chartData: ChartData
grid: boolean
dataEmpty: boolean
}) {
const showTempChart = chartData.systemStats.at(-1)?.stats.t
const filter = useStore($temperatureFilter)
const userSettings = useStore($userSettings)
const statsRef = useRef(chartData.systemStats)
statsRef.current = chartData.systemStats
// Derive sensor names key from latest data point
let sensorNamesKey = ""
for (let i = chartData.systemStats.length - 1; i >= 0; i--) {
const t = chartData.systemStats[i].stats?.t
if (t) {
sensorNamesKey = Object.keys(t).sort().join("\0")
break
}
}
// Only recompute colors and dataKey functions when sensor names change
const { colorMap, dataKeys, sortedKeys } = useMemo(() => {
const stats = statsRef.current
const tempSums = {} as Record<string, number>
for (const data of stats) {
const t = data.stats?.t
if (!t) continue
for (const key of Object.keys(t)) {
tempSums[key] = (tempSums[key] ?? 0) + t[key]
}
}
const sorted = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a])
const colorMap = {} as Record<string, string>
const dataKeys = {} as Record<string, (d: SystemStatsRecord) => number | undefined>
for (let i = 0; i < sorted.length; i++) {
const key = sorted[i]
colorMap[key] = `hsl(${((i * 360) / sorted.length) % 360}, 60%, 55%)`
dataKeys[key] = (d: SystemStatsRecord) => d.stats?.t?.[key]
}
return { colorMap, dataKeys, sortedKeys: sorted }
}, [sensorNamesKey])
const dataPoints = useMemo(() => {
return sortedKeys.map((key) => {
const filterTerms = filter
? filter
.toLowerCase()
.split(" ")
.filter((term) => term.length > 0)
: []
const filtered = filterTerms.length > 0 && !filterTerms.some((term) => key.toLowerCase().includes(term))
const strokeOpacity = filtered ? 0.1 : 1
return {
label: key,
dataKey: dataKeys[key],
color: colorMap[key],
opacity: strokeOpacity,
}
})
}, [sortedKeys, filter, dataKeys, colorMap])
if (!showTempChart) {
return null
}
const legend = Object.keys(chartData.systemStats.at(-1)?.stats.t ?? {}).length < 12
return (
<div className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}>
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Temperature`}
description={t`Temperatures of system sensors`}
cornerEl={<FilterBar store={$temperatureFilter} />}
legend={legend}
>
<LineChartDefault
chartData={chartData}
itemSorter={(a, b) => b.value - a.value}
domain={["auto", "auto"]}
legend={legend}
tickFormatter={(val) => {
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
return `${toFixedFloat(value, 2)} ${unit}`
}}
contentFormatter={(item) => {
const { value, unit } = formatTemperature(item.value, userSettings.unitTemp)
return `${decimalString(value)} ${unit}`
}}
dataPoints={dataPoints}
></LineChartDefault>
</ChartCard>
</div>
)
}