mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-13 00:11:49 +02:00
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:
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from "react"
|
||||
import { type ReactNode, useEffect, useMemo, useState } from "react"
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import {
|
||||
ChartContainer,
|
||||
@@ -11,18 +11,23 @@ import {
|
||||
import { chartMargin, cn, formatShortDate } from "@/lib/utils"
|
||||
import type { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
import { AxisDomain } from "recharts/types/util/types"
|
||||
import type { AxisDomain } from "recharts/types/util/types"
|
||||
import { useIntersectionObserver } from "@/lib/use-intersection-observer"
|
||||
|
||||
export type DataPoint = {
|
||||
export type DataPoint<T = SystemStatsRecord> = {
|
||||
label: string
|
||||
dataKey: (data: SystemStatsRecord) => number | undefined
|
||||
dataKey: (data: T) => number | null | undefined
|
||||
color: number | string
|
||||
opacity: number
|
||||
stackId?: string | number
|
||||
order?: number
|
||||
strokeOpacity?: number
|
||||
activeDot?: boolean
|
||||
}
|
||||
|
||||
export default function AreaChartDefault({
|
||||
chartData,
|
||||
customData,
|
||||
max,
|
||||
maxToggled,
|
||||
tickFormatter,
|
||||
@@ -34,96 +39,129 @@ export default function AreaChartDefault({
|
||||
showTotal = false,
|
||||
reverseStackOrder = false,
|
||||
hideYAxis = false,
|
||||
filter,
|
||||
truncate = false,
|
||||
}: // logRender = false,
|
||||
{
|
||||
chartData: ChartData
|
||||
max?: number
|
||||
maxToggled?: boolean
|
||||
tickFormatter: (value: number, index: number) => string
|
||||
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
|
||||
dataPoints?: DataPoint[]
|
||||
domain?: AxisDomain
|
||||
legend?: boolean
|
||||
showTotal?: boolean
|
||||
itemSorter?: (a: any, b: any) => number
|
||||
reverseStackOrder?: boolean
|
||||
hideYAxis?: boolean
|
||||
// logRender?: boolean
|
||||
}) {
|
||||
{
|
||||
chartData: ChartData
|
||||
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
|
||||
customData?: any[]
|
||||
max?: number
|
||||
maxToggled?: boolean
|
||||
tickFormatter: (value: number, index: number) => string
|
||||
// biome-ignore lint/suspicious/noExplicitAny: recharts tooltip item interop
|
||||
contentFormatter: (item: any, key: string) => ReactNode
|
||||
// biome-ignore lint/suspicious/noExplicitAny: accepts DataPoint with different generic types
|
||||
dataPoints?: DataPoint<any>[]
|
||||
domain?: AxisDomain
|
||||
legend?: boolean
|
||||
showTotal?: boolean
|
||||
// biome-ignore lint/suspicious/noExplicitAny: recharts tooltip item interop
|
||||
itemSorter?: (a: any, b: any) => number
|
||||
reverseStackOrder?: boolean
|
||||
hideYAxis?: boolean
|
||||
filter?: string
|
||||
truncate?: boolean
|
||||
// logRender?: boolean
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
||||
const sourceData = customData ?? chartData.systemStats
|
||||
// Only update the rendered data while the chart is visible
|
||||
const [displayData, setDisplayData] = useState(sourceData)
|
||||
|
||||
// Reduce chart redraws by only updating while visible or when chart time changes
|
||||
useEffect(() => {
|
||||
const shouldPrimeData = sourceData.length && !displayData.length
|
||||
const sourceChanged = sourceData !== displayData
|
||||
const shouldUpdate = shouldPrimeData || (sourceChanged && isIntersecting)
|
||||
if (shouldUpdate) {
|
||||
setDisplayData(sourceData)
|
||||
}
|
||||
}, [displayData, isIntersecting, sourceData])
|
||||
|
||||
// Use a stable key derived from data point identities and visual properties
|
||||
const areasKey = dataPoints?.map((d) => `${d.label}:${d.opacity}`).join("\0")
|
||||
|
||||
const Areas = useMemo(() => {
|
||||
return dataPoints?.map((dataPoint, i) => {
|
||||
let { color } = dataPoint
|
||||
if (typeof color === "number") {
|
||||
color = `var(--chart-${color})`
|
||||
}
|
||||
return (
|
||||
<Area
|
||||
key={dataPoint.label}
|
||||
dataKey={dataPoint.dataKey}
|
||||
name={dataPoint.label}
|
||||
type="monotoneX"
|
||||
fill={color}
|
||||
fillOpacity={dataPoint.opacity}
|
||||
stroke={color}
|
||||
strokeOpacity={dataPoint.strokeOpacity}
|
||||
isAnimationActive={false}
|
||||
stackId={dataPoint.stackId}
|
||||
order={dataPoint.order || i}
|
||||
activeDot={dataPoint.activeDot ?? true}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}, [areasKey, maxToggled])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: ignore
|
||||
return useMemo(() => {
|
||||
if (chartData.systemStats.length === 0) {
|
||||
if (displayData.length === 0) {
|
||||
return null
|
||||
}
|
||||
// if (logRender) {
|
||||
// console.log("Rendered at", new Date())
|
||||
console.log("Rendered at", new Date(), "for", dataPoints?.at(0)?.label)
|
||||
// }
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth || hideYAxis,
|
||||
"ps-4": hideYAxis,
|
||||
})}
|
||||
<ChartContainer
|
||||
ref={ref}
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth || hideYAxis,
|
||||
"ps-4": hideYAxis,
|
||||
})}
|
||||
>
|
||||
<AreaChart
|
||||
reverseStackOrder={reverseStackOrder}
|
||||
accessibilityLayer
|
||||
data={displayData}
|
||||
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
|
||||
>
|
||||
<AreaChart
|
||||
reverseStackOrder={reverseStackOrder}
|
||||
accessibilityLayer
|
||||
data={chartData.systemStats}
|
||||
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
{!hideYAxis && (
|
||||
<YAxis
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
domain={domain ?? [0, max ?? "auto"]}
|
||||
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
)}
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
// @ts-expect-error
|
||||
itemSorter={itemSorter}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={contentFormatter}
|
||||
showTotal={showTotal}
|
||||
/>
|
||||
}
|
||||
<CartesianGrid vertical={false} />
|
||||
{!hideYAxis && (
|
||||
<YAxis
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
domain={domain ?? [0, max ?? "auto"]}
|
||||
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{dataPoints?.map((dataPoint) => {
|
||||
let { color } = dataPoint
|
||||
if (typeof color === "number") {
|
||||
color = `var(--chart-${color})`
|
||||
}
|
||||
return (
|
||||
<Area
|
||||
key={dataPoint.label}
|
||||
dataKey={dataPoint.dataKey}
|
||||
name={dataPoint.label}
|
||||
type="monotoneX"
|
||||
fill={color}
|
||||
fillOpacity={dataPoint.opacity}
|
||||
stroke={color}
|
||||
isAnimationActive={false}
|
||||
stackId={dataPoint.stackId}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{legend && <ChartLegend content={<ChartLegendContent reverse={reverseStackOrder} />} />}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)}
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
// @ts-expect-error
|
||||
itemSorter={itemSorter}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={contentFormatter}
|
||||
showTotal={showTotal}
|
||||
filter={filter}
|
||||
truncate={truncate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{Areas}
|
||||
{legend && <ChartLegend content={<ChartLegendContent />} />}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled, showTotal])
|
||||
}, [displayData, yAxisWidth, showTotal, filter])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user