add max 1m values for cpu, bandwidth, disk io

* removes unused things from chart.tsx
* updates y axis width only if it grows
* add generic area chart component and remove individual cpu, bandwidth, disk io charts
This commit is contained in:
Henry Dollman
2024-10-10 15:11:48 -04:00
parent 76cea9d3c3
commit cedf80a869
18 changed files with 441 additions and 425 deletions

View File

@@ -1,7 +1,7 @@
import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores'
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card'
import { useStore } from '@nanostores/react'
import Spinner from '../spinner'
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
@@ -12,16 +12,15 @@ import { scaleTime } from 'd3-scale'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { Button, buttonVariants } from '../ui/button'
import { Input } from '../ui/input'
import { Rows, TuxIcon } from '../ui/icons'
import { ChartAverage, ChartMax, Rows, TuxIcon } from '../ui/icons'
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
const CpuChart = lazy(() => import('../charts/cpu-chart'))
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
const MemChart = lazy(() => import('../charts/mem-chart'))
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
const DiskChart = lazy(() => import('../charts/disk-chart'))
const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
const AreaChartDefault = lazy(() => import('../charts/area-chart'))
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
const SwapChart = lazy(() => import('../charts/swap-chart'))
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
@@ -29,11 +28,16 @@ const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
export default function SystemDetail({ name }: { name: string }) {
const systems = useStore($systems)
const chartTime = useStore($chartTime)
/** Max CPU toggle value */
const cpuMaxStore = useState(false)
const bandwidthMaxStore = useState(false)
const diskIoMaxStore = useState(false)
const [grid, setGrid] = useLocalStorage('grid', true)
const [ticks, setTicks] = useState([] as number[])
const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const netCardRef = useRef<HTMLDivElement>(null)
const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element)
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
[]
)
@@ -43,15 +47,18 @@ export default function SystemDetail({ name }: { name: string }) {
const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>(
[]
)
const hasDockerStats = dockerCpuChartData.length > 0
const isLongerChart = chartTime !== '1h'
useEffect(() => {
document.title = `${name} / Beszel`
return () => {
resetCharts()
$chartTime.set($userSettings.get().chartTime)
setContainerFilterBar(null)
$containerFilter.set('')
// setHasDocker(false)
cpuMaxStore[1](false)
bandwidthMaxStore[1](false)
diskIoMaxStore[1](false)
}
}, [name])
@@ -133,12 +140,15 @@ export default function SystemDetail({ name }: { name: string }) {
getStats<ContainerStatsRecord>('container_stats'),
]).then(([systemStats, containerStats]) => {
const expectedInterval = chartTimeData[chartTime].expectedInterval
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
makeContainerData(addEmptyValues(containerStats.value, expectedInterval))
}
if (systemStats.status === 'fulfilled') {
setSystemStats(addEmptyValues(systemStats.value, expectedInterval))
}
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
!containerFilterBar && setContainerFilterBar(<ContainerFilterBar />)
makeContainerData(addEmptyValues(containerStats.value, expectedInterval))
} else {
setContainerFilterBar(null)
}
})
}, [system, chartTime])
@@ -149,7 +159,10 @@ export default function SystemDetail({ name }: { name: string }) {
const now = new Date()
const startTime = chartTimeData[chartTime].getOffset(now)
const scale = scaleTime([startTime.getTime(), now], [0, systemStats.length])
setTicks(scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime()))
const newTicks = scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime())
if (newTicks[0] !== ticks[0]) {
setTicks(newTicks)
}
}, [chartTime, systemStats])
// make container stats for charts
@@ -192,7 +205,7 @@ export default function SystemDetail({ name }: { name: string }) {
let uptime: number | string = system.info.u
if (system.info.u < 172800) {
const hours = Math.trunc(uptime / 3600)
uptime = `${hours} hour${hours > 1 ? 's' : ''}`
uptime = `${hours} hour${hours == 1 ? '' : 's'}`
} else {
uptime = `${Math.trunc(system.info?.u / 86400)} days`
}
@@ -239,7 +252,7 @@ export default function SystemDetail({ name }: { name: string }) {
return (
<>
<div id="chartwrap" className="grid gap-4 mb-10">
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
{/* system info */}
<Card>
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
@@ -324,17 +337,27 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
grid={grid}
title="Total CPU Usage"
description="Average system-wide CPU utilization"
description={`${
cpuMaxStore[0] && isLongerChart ? 'Max 1 min ' : 'Average'
} system-wide CPU utilization`}
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
>
<CpuChart ticks={ticks} systemData={systemStats} />
<AreaChartDefault
ticks={ticks}
systemData={systemStats}
chartName="CPU Usage"
showMax={isLongerChart && cpuMaxStore[0]}
unit="%"
chartTime={chartTime}
/>
</ChartCard>
{hasDockerStats && (
{containerFilterBar && (
<ChartCard
grid={grid}
title="Docker CPU Usage"
description="CPU utilization of docker containers"
isContainerChart={true}
description="Average CPU utilization of containers"
cornerEl={containerFilterBar}
>
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
</ChartCard>
@@ -348,12 +371,12 @@ export default function SystemDetail({ name }: { name: string }) {
<MemChart ticks={ticks} systemData={systemStats} />
</ChartCard>
{hasDockerStats && (
{containerFilterBar && (
<ChartCard
grid={grid}
title="Docker Memory Usage"
description="Memory usage of docker containers"
isContainerChart={true}
cornerEl={containerFilterBar}
>
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
</ChartCard>
@@ -368,23 +391,37 @@ export default function SystemDetail({ name }: { name: string }) {
/>
</ChartCard>
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
<DiskIoChart
<ChartCard
grid={grid}
title="Disk I/O"
description="Throughput of root filesystem"
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
>
<AreaChartDefault
ticks={ticks}
systemData={systemStats}
dataKeys={['stats.dw', 'stats.dr']}
showMax={isLongerChart && diskIoMaxStore[0]}
chartName="dio"
chartTime={chartTime}
/>
</ChartCard>
<ChartCard
grid={grid}
title="Bandwidth"
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
description="Network traffic of public interfaces"
>
<BandwidthChart ticks={ticks} systemData={systemStats} />
<AreaChartDefault
ticks={ticks}
systemData={systemStats}
showMax={isLongerChart && bandwidthMaxStore[0]}
chartName="bw"
chartTime={chartTime}
/>
</ChartCard>
{hasDockerStats && dockerNetChartData.length > 0 && (
{containerFilterBar && dockerNetChartData.length > 0 && (
<div
ref={netCardRef}
className={cn({
@@ -394,7 +431,7 @@ export default function SystemDetail({ name }: { name: string }) {
<ChartCard
title="Docker Network I/O"
description="Includes traffic between internal services"
isContainerChart={true}
cornerEl={containerFilterBar}
>
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
</ChartCard>
@@ -436,11 +473,14 @@ export default function SystemDetail({ name }: { name: string }) {
grid={grid}
title={`${extraFsName} I/O`}
description={`Throughput of ${extraFsName}`}
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
>
<DiskIoChart
<AreaChartDefault
ticks={ticks}
systemData={systemStats}
dataKeys={[`stats.efs.${extraFsName}.w`, `stats.efs.${extraFsName}.r`]}
showMax={isLongerChart && diskIoMaxStore[0]}
chartName={`efs.${extraFsName}`}
chartTime={chartTime}
/>
</ChartCard>
</div>
@@ -461,10 +501,10 @@ function ContainerFilterBar() {
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
$containerFilter.set(e.target.value)
}, []) // Use an empty dependency array to prevent re-creation
}, [])
return (
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
<>
<Input
placeholder="Filter..."
className="pl-4 pr-8"
@@ -483,7 +523,33 @@ function ContainerFilterBar() {
<XIcon className="h-4 w-4" />
</Button>
)}
</div>
</>
)
}
function SelectAvgMax({
store,
}: {
store: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
}) {
const [max, setMax] = store
const Icon = max ? ChartMax : ChartAverage
return (
<Select value={max ? 'max' : 'avg'} onValueChange={(e) => setMax(e === 'max')}>
<SelectTrigger className="relative pl-10 pr-5">
<Icon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-85" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem key="avg" value="avg">
Average
</SelectItem>
<SelectItem key="max" value="max">
Max 1 min
</SelectItem>
</SelectContent>
</Select>
)
}
@@ -492,13 +558,13 @@ function ChartCard({
description,
children,
grid,
isContainerChart,
cornerEl,
}: {
title: string
description: string
children: React.ReactNode
grid?: boolean
isContainerChart?: boolean
cornerEl?: JSX.Element | null
}) {
const { isIntersecting, ref } = useIntersectionObserver()
@@ -510,12 +576,16 @@ function ChartCard({
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
{isContainerChart && <ContainerFilterBar />}
{cornerEl && (
<div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:right-3.5">
{cornerEl}
</div>
)}
</CardHeader>
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
{<Spinner />}
{isIntersecting && <Suspense>{children}</Suspense>}
</CardContent>
</div>
</Card>
)
}