lazy load charts and disable chart animations

This commit is contained in:
Henry Dollman
2024-08-04 20:14:13 -04:00
parent e3ed07a999
commit 8ef30e0733
12 changed files with 189 additions and 105 deletions

Binary file not shown.

View File

@@ -37,6 +37,7 @@
"recharts": "^2.13.0-alpha.4", "recharts": "^2.13.0-alpha.4",
"tailwind-merge": "^2.4.0", "tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"use-is-in-viewport": "^1.0.9",
"valibot": "^0.36.0" "valibot": "^0.36.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,12 +1,12 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types' import { SystemStatsRecord } from '@/types'
import { useRef } from 'react' import { useMemo, useRef } from 'react'
export default function BandwidthChart({ export default function BandwidthChart({
ticks, ticks,
@@ -19,13 +19,17 @@ export default function BandwidthChart({
const yAxisWidth = useYaxisWidth(chartRef) const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime) const chartTime = useStore($chartTime)
if (!systemData.length || !ticks.length) { const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
return <Spinner />
}
return ( return (
<div ref={chartRef}> <div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto"> {/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={systemData} data={systemData}
@@ -80,7 +84,8 @@ export default function BandwidthChart({
fill="hsl(var(--chart-5))" fill="hsl(var(--chart-5))"
fillOpacity={0.4} fillOpacity={0.4}
stroke="hsl(var(--chart-5))" stroke="hsl(var(--chart-5))"
animationDuration={1200} // animationDuration={1200}
isAnimationActive={false}
/> />
<Area <Area
dataKey="stats.nr" dataKey="stats.nr"
@@ -89,7 +94,8 @@ export default function BandwidthChart({
fill="hsl(var(--chart-2))" fill="hsl(var(--chart-2))"
fillOpacity={0.4} fillOpacity={0.4}
stroke="hsl(var(--chart-2))" stroke="hsl(var(--chart-2))"
animationDuration={1200} // animationDuration={1200}
isAnimationActive={false}
/> />
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>

View File

@@ -6,8 +6,8 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
@@ -22,6 +22,8 @@ export default function ContainerCpuChart({
const yAxisWidth = useYaxisWidth(chartRef) const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime) const chartTime = useStore($chartTime)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
let config = {} as Record< let config = {} as Record<
string, string,
@@ -57,13 +59,19 @@ export default function ContainerCpuChart({
return config satisfies ChartConfig return config satisfies ChartConfig
}, [chartData]) }, [chartData])
if (!chartData.length || !ticks.length) { // if (!chartData.length || !ticks.length) {
return <Spinner /> // return <Spinner />
} // }
return ( return (
<div ref={chartRef}> <div ref={chartRef}>
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto"> {/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={chartData} data={chartData}
@@ -105,8 +113,9 @@ export default function ContainerCpuChart({
<Area <Area
key={key} key={key}
// isAnimationActive={chartData.length < 20} // isAnimationActive={chartData.length < 20}
animateNewValues={false} isAnimationActive={false}
animationDuration={1200} // animateNewValues={false}
// animationDuration={1200}
dataKey={key} dataKey={key}
type="monotoneX" type="monotoneX"
fill={chartConfig[key].color} fill={chartConfig[key].color}

View File

@@ -6,8 +6,8 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
@@ -18,9 +18,11 @@ export default function ContainerMemChart({
chartData: Record<string, number | string>[] chartData: Record<string, number | string>[]
ticks: number[] ticks: number[]
}) { }) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null) const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef) const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
let config = {} as Record< let config = {} as Record<
@@ -57,13 +59,19 @@ export default function ContainerMemChart({
return config satisfies ChartConfig return config satisfies ChartConfig
}, [chartData]) }, [chartData])
if (!chartData.length || !ticks.length) { // if (!chartData.length || !ticks.length) {
return <Spinner /> // return <Spinner />
} // }
return ( return (
<div ref={chartRef}> <div ref={chartRef}>
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto"> {/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={chartData} data={chartData}
@@ -71,8 +79,6 @@ export default function ContainerMemChart({
margin={{ margin={{
top: 10, top: 10,
}} }}
// reverseStackOrder={true}
> >
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<YAxis <YAxis
@@ -109,9 +115,8 @@ export default function ContainerMemChart({
{Object.keys(chartConfig).map((key) => ( {Object.keys(chartConfig).map((key) => (
<Area <Area
key={key} key={key}
isAnimationActive={chartData.length < 20} // animationDuration={1200}
animateNewValues={false} isAnimationActive={false}
animationDuration={1200}
dataKey={key} dataKey={key}
type="monotoneX" type="monotoneX"
fill={chartConfig[key].color} fill={chartConfig[key].color}

View File

@@ -6,8 +6,8 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from '@/components/ui/chart' } from '@/components/ui/chart'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
@@ -19,9 +19,11 @@ export default function ContainerCpuChart({
chartData: Record<string, number | number[]>[] chartData: Record<string, number | number[]>[]
ticks: number[] ticks: number[]
}) { }) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null) const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef) const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
let config = {} as Record< let config = {} as Record<
@@ -57,13 +59,19 @@ export default function ContainerCpuChart({
return config satisfies ChartConfig return config satisfies ChartConfig
}, [chartData]) }, [chartData])
if (!chartData.length || !ticks.length) { // if (!chartData.length || !ticks.length) {
return <Spinner /> // return <Spinner />
} // }
return ( return (
<div ref={chartRef}> <div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto"> {/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={chartData} data={chartData}
@@ -134,9 +142,8 @@ export default function ContainerCpuChart({
<Area <Area
key={key} key={key}
name={key} name={key}
// isAnimationActive={chartData.length < 20} // animationDuration={1200}
animateNewValues={false} isAnimationActive={false}
animationDuration={1200}
dataKey={(data) => data?.[key]?.[2] ?? 0} dataKey={(data) => data?.[key]?.[2] ?? 0}
type="monotoneX" type="monotoneX"
fill={chartConfig[key].color} fill={chartConfig[key].color}

View File

@@ -1,12 +1,12 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types' import { SystemStatsRecord } from '@/types'
import { useRef } from 'react' import { useMemo, useRef } from 'react'
export default function CpuChart({ export default function CpuChart({
ticks, ticks,
@@ -15,17 +15,24 @@ export default function CpuChart({
ticks: number[] ticks: number[]
systemData: SystemStatsRecord[] systemData: SystemStatsRecord[]
}) { }) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null) const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef) const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
if (!systemData.length || !ticks.length) { const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
return <Spinner />
} // if (!systemData.length || !ticks.length) {
// return <Spinner />
// }
return ( return (
<div ref={chartRef}> <div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto"> <ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart accessibilityLayer data={systemData} margin={{ top: 10 }}> <AreaChart accessibilityLayer data={systemData} margin={{ top: 10 }}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<YAxis <YAxis
@@ -64,9 +71,10 @@ export default function CpuChart({
fill="hsl(var(--chart-1))" fill="hsl(var(--chart-1))"
fillOpacity={0.4} fillOpacity={0.4}
stroke="hsl(var(--chart-1))" stroke="hsl(var(--chart-1))"
animationDuration={1200} isAnimationActive={false}
// animationEasing="ease-out" // animationEasing="ease-out"
// animateNewValues={false} animationDuration={700}
animateNewValues={true}
/> />
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>

View File

@@ -1,9 +1,9 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types' import { SystemStatsRecord } from '@/types'
@@ -15,9 +15,11 @@ export default function DiskChart({
ticks: number[] ticks: number[]
systemData: SystemStatsRecord[] systemData: SystemStatsRecord[]
}) { }) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null) const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef) const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const diskSize = useMemo(() => { const diskSize = useMemo(() => {
return Math.round(systemData[0]?.stats.d) return Math.round(systemData[0]?.stats.d)
@@ -32,13 +34,19 @@ export default function DiskChart({
// return ticks // return ticks
// }, [diskSize]) // }, [diskSize])
if (!systemData.length || !ticks.length) { // if (!systemData.length || !ticks.length) {
return <Spinner /> // return <Spinner />
} // }
return ( return (
<div ref={chartRef}> <div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto"> {/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={systemData} data={systemData}
@@ -88,7 +96,8 @@ export default function DiskChart({
fill="hsl(var(--chart-4))" fill="hsl(var(--chart-4))"
fillOpacity={0.4} fillOpacity={0.4}
stroke="hsl(var(--chart-4))" stroke="hsl(var(--chart-4))"
animationDuration={1200} // animationDuration={1200}
isAnimationActive={false}
/> />
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>

View File

@@ -1,12 +1,12 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types' import { SystemStatsRecord } from '@/types'
import { useRef } from 'react' import { useMemo, useRef } from 'react'
export default function DiskIoChart({ export default function DiskIoChart({
ticks, ticks,
@@ -15,17 +15,25 @@ export default function DiskIoChart({
ticks: number[] ticks: number[]
systemData: SystemStatsRecord[] systemData: SystemStatsRecord[]
}) { }) {
const chartTime = useStore($chartTime)
const chartRef = useRef<HTMLDivElement>(null) const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef) const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
if (!systemData.length || !ticks.length) { const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
return <Spinner />
} // if (!systemData.length || !ticks.length) {
// return <Spinner />
// }
return ( return (
<div ref={chartRef}> <div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto"> {/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={systemData} data={systemData}
@@ -80,7 +88,8 @@ export default function DiskIoChart({
fill="hsl(var(--chart-3))" fill="hsl(var(--chart-3))"
fillOpacity={0.4} fillOpacity={0.4}
stroke="hsl(var(--chart-3))" stroke="hsl(var(--chart-3))"
animationDuration={1200} // animationDuration={1200}
isAnimationActive={false}
/> />
<Area <Area
dataKey="stats.dr" dataKey="stats.dr"
@@ -89,7 +98,8 @@ export default function DiskIoChart({
fill="hsl(var(--chart-1))" fill="hsl(var(--chart-1))"
fillOpacity={0.4} fillOpacity={0.4}
stroke="hsl(var(--chart-1))" stroke="hsl(var(--chart-1))"
animationDuration={1200} // animationDuration={1200}
isAnimationActive={false}
/> />
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>

View File

@@ -1,9 +1,9 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores' import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types' import { SystemStatsRecord } from '@/types'
@@ -19,18 +19,26 @@ export default function MemChart({
const chartRef = useRef<HTMLDivElement>(null) const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef) const yAxisWidth = useYaxisWidth(chartRef)
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
const totalMem = useMemo(() => { const totalMem = useMemo(() => {
const maxMem = Math.ceil(systemData[0]?.stats.m) const maxMem = Math.ceil(systemData[0]?.stats.m)
return maxMem > 2 && maxMem % 2 !== 0 ? maxMem + 1 : maxMem return maxMem > 2 && maxMem % 2 !== 0 ? maxMem + 1 : maxMem
}, [systemData]) }, [systemData])
if (!systemData.length || !ticks.length) { // if (!systemData.length || !ticks.length) {
return <Spinner /> // return <Spinner />
} // }
return ( return (
<div ref={chartRef}> <div ref={chartRef}>
<ChartContainer config={{}} className="h-full w-full absolute aspect-auto"> {/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={systemData} data={systemData}
@@ -80,7 +88,8 @@ export default function MemChart({
fillOpacity={0.4} fillOpacity={0.4}
stroke="hsl(var(--chart-2))" stroke="hsl(var(--chart-2))"
stackId="a" stackId="a"
animationDuration={1200} // animationDuration={1200}
isAnimationActive={false}
/> />
<Area <Area
dataKey="stats.mb" dataKey="stats.mb"
@@ -91,7 +100,8 @@ export default function MemChart({
strokeOpacity={0.3} strokeOpacity={0.3}
stroke="hsl(var(--chart-2))" stroke="hsl(var(--chart-2))"
stackId="a" stackId="a"
animationDuration={1200} // animationDuration={1200}
isAnimationActive={false}
/> />
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>

View File

@@ -1,12 +1,12 @@
import { $updatedSystem, $systems, pb, $chartTime } from '@/lib/stores' import { $updatedSystem, $systems, pb, $chartTime } from '@/lib/stores'
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types' import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
import { Suspense, lazy, useCallback, useEffect, useMemo, useState } from 'react' import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import Spinner from '../spinner' import Spinner from '../spinner'
import { ClockArrowUp, CpuIcon, GlobeIcon } from 'lucide-react' import { ClockArrowUp, CpuIcon, GlobeIcon } from 'lucide-react'
import ChartTimeSelect from '../charts/chart-time-select' import ChartTimeSelect from '../charts/chart-time-select'
import { chartTimeData, cn, getPbTimestamp } from '@/lib/utils' import { chartTimeData, cn, getPbTimestamp, useClampedIsInViewport } from '@/lib/utils'
import { Separator } from '../ui/separator' import { Separator } from '../ui/separator'
import { scaleTime } from 'd3-scale' import { scaleTime } from 'd3-scale'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
@@ -27,15 +27,10 @@ export default function ServerDetail({ name }: { name: string }) {
const [ticks, setTicks] = useState([] as number[]) const [ticks, setTicks] = useState([] as number[])
const [server, setServer] = useState({} as SystemRecord) const [server, setServer] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[]) const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [dockerCpuChartData, setDockerCpuChartData] = useState( const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>()
[] as Record<string, number | string>[] const [dockerMemChartData, setDockerMemChartData] = useState<Record<string, number | string>[]>()
) const [dockerNetChartData, setDockerNetChartData] =
const [dockerMemChartData, setDockerMemChartData] = useState( useState<Record<string, number | number[]>[]>()
[] as Record<string, number | string>[]
)
const [dockerNetChartData, setDockerNetChartData] = useState(
[] as Record<string, number | number[]>[]
)
useEffect(() => { useEffect(() => {
document.title = `${name} / Beszel` document.title = `${name} / Beszel`
@@ -47,9 +42,9 @@ export default function ServerDetail({ name }: { name: string }) {
const resetCharts = useCallback(() => { const resetCharts = useCallback(() => {
setSystemStats([]) setSystemStats([])
setDockerCpuChartData([]) setDockerCpuChartData(undefined)
setDockerMemChartData([]) setDockerMemChartData(undefined)
setDockerNetChartData([]) setDockerNetChartData(undefined)
}, []) }, [])
useEffect(resetCharts, [chartTime]) useEffect(resetCharts, [chartTime])
@@ -121,7 +116,9 @@ export default function ServerDetail({ name }: { name: string }) {
sort: 'created', sort: 'created',
}) })
.then((records) => { .then((records) => {
if (records.length) {
makeContainerData(records) makeContainerData(records)
}
// setContainers(records) // setContainers(records)
}) })
}, [server, chartTime]) }, [server, chartTime])
@@ -129,15 +126,14 @@ export default function ServerDetail({ name }: { name: string }) {
// container stats for charts // container stats for charts
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => { const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
// console.log('containers', containers) // console.log('containers', containers)
const dockerCpuData = [] as typeof dockerCpuChartData const dockerCpuData = []
const dockerMemData = [] as typeof dockerMemChartData const dockerMemData = []
const dockerNetData = [] as typeof dockerNetChartData const dockerNetData = []
for (let { created, stats } of containers) { for (let { created, stats } of containers) {
const time = new Date(created).getTime() const time = new Date(created).getTime()
let cpuData = { time } as (typeof dockerCpuChartData)[0] let cpuData = { time } as Record<string, number | string>
let memData = { time } as (typeof dockerMemChartData)[0] let memData = { time } as Record<string, number | string>
let netData = { time } as (typeof dockerNetChartData)[0] let netData = { time } as Record<string, number | number[]>
for (let container of stats) { for (let container of stats) {
cpuData[container.n] = container.c cpuData[container.n] = container.c
memData[container.n] = container.m memData[container.n] = container.m
@@ -225,7 +221,7 @@ export default function ServerDetail({ name }: { name: string }) {
<CpuChart ticks={ticks} systemData={systemStats} /> <CpuChart ticks={ticks} systemData={systemStats} />
</ChartCard> </ChartCard>
{dockerCpuChartData.length > 0 && ( {dockerCpuChartData && (
<ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers"> <ChartCard title="Docker CPU Usage" description="CPU utilization of docker containers">
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} /> <ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
</ChartCard> </ChartCard>
@@ -235,7 +231,7 @@ export default function ServerDetail({ name }: { name: string }) {
<MemChart ticks={ticks} systemData={systemStats} /> <MemChart ticks={ticks} systemData={systemStats} />
</ChartCard> </ChartCard>
{dockerMemChartData.length > 0 && ( {dockerMemChartData && (
<ChartCard title="Docker Memory Usage" description="Memory usage of docker containers"> <ChartCard title="Docker Memory Usage" description="Memory usage of docker containers">
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} /> <ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
</ChartCard> </ChartCard>
@@ -256,7 +252,7 @@ export default function ServerDetail({ name }: { name: string }) {
<BandwidthChart ticks={ticks} systemData={systemStats} /> <BandwidthChart ticks={ticks} systemData={systemStats} />
</ChartCard> </ChartCard>
{dockerNetChartData.length > 0 && ( {dockerNetChartData && (
<ChartCard <ChartCard
title="Docker Network I/O" title="Docker Network I/O"
description="Includes traffic between internal services" description="Includes traffic between internal services"
@@ -277,8 +273,10 @@ function ChartCard({
description: string description: string
children: React.ReactNode children: React.ReactNode
}) { }) {
const target = useRef<HTMLDivElement>(null)
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
return ( return (
<Card className="pb-2 sm:pb-4 col-span-full"> <Card className="pb-2 sm:pb-4 col-span-full" ref={wrappedTargetRef}>
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4"> <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> <CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
<CardDescription>{description}</CardDescription> <CardDescription>{description}</CardDescription>
@@ -287,7 +285,8 @@ function ChartCard({
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pl-1 w-[calc(100%-1.6em)] h-52 relative"> <CardContent className="pl-1 w-[calc(100%-1.6em)] h-52 relative">
<Suspense fallback={<Spinner />}>{children}</Suspense> {<Spinner />}
{isInViewport && <Suspense>{children}</Suspense>}
</CardContent> </CardContent>
</Card> </Card>
) )

View File

@@ -7,6 +7,7 @@ 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'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import useIsInViewport, { CallbackRef, HookOptions } from 'use-is-in-viewport'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -183,17 +184,36 @@ export const chartTimeData: ChartTimeData = {
/** Hacky solution to set the correct width of the yAxis in recharts */ /** Hacky solution to set the correct width of the yAxis in recharts */
export function useYaxisWidth(chartRef: React.RefObject<HTMLDivElement>) { export function useYaxisWidth(chartRef: React.RefObject<HTMLDivElement>) {
const [yAxisWidth, setYAxisWidth] = useState(90) const [yAxisWidth, setYAxisWidth] = useState(180)
useEffect(() => { useEffect(() => {
let interval = setInterval(() => { let interval = setInterval(() => {
// console.log('chartRef', chartRef.current) // console.log('chartRef', chartRef.current)
const yAxisElement = chartRef?.current?.querySelector('.yAxis') const yAxisElement = chartRef?.current?.querySelector('.yAxis')
if (yAxisElement) { if (yAxisElement) {
console.log('yAxisElement', yAxisElement) // console.log('yAxisElement', yAxisElement)
setYAxisWidth(yAxisElement.getBoundingClientRect().width + 22) setYAxisWidth(yAxisElement.getBoundingClientRect().width + 22)
clearInterval(interval) clearInterval(interval)
} }
}, 16) }, 0)
return () => clearInterval(interval)
}, []) }, [])
return yAxisWidth return yAxisWidth
} }
export function useClampedIsInViewport(options: HookOptions): [boolean | null, CallbackRef] {
const [isInViewport, wrappedTargetRef] = useIsInViewport(options)
const [wasInViewportAtleastOnce, setWasInViewportAtleastOnce] = useState(isInViewport)
useEffect(() => {
setWasInViewportAtleastOnce((prev) => {
// this will clamp it to the first true
// received from useIsInViewport
if (!prev) {
return isInViewport
}
return prev
})
}, [isInViewport])
return [wasInViewportAtleastOnce, wrappedTargetRef]
}