From 8ef30e073319ee7437fe5beab90fcb7fe4ce6bed Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Sun, 4 Aug 2024 20:14:13 -0400 Subject: [PATCH] lazy load charts and disable chart animations --- hub/site/bun.lockb | Bin 146137 -> 146961 bytes hub/site/package.json | 1 + .../src/components/charts/bandwidth-chart.tsx | 24 +++++--- .../components/charts/container-cpu-chart.tsx | 25 ++++++--- .../components/charts/container-mem-chart.tsx | 29 ++++++---- .../components/charts/container-net-chart.tsx | 27 +++++---- hub/site/src/components/charts/cpu-chart.tsx | 28 +++++---- hub/site/src/components/charts/disk-chart.tsx | 25 ++++++--- .../src/components/charts/disk-io-chart.tsx | 30 ++++++---- hub/site/src/components/charts/mem-chart.tsx | 26 ++++++--- hub/site/src/components/routes/system.tsx | 53 +++++++++--------- hub/site/src/lib/utils.ts | 26 ++++++++- 12 files changed, 189 insertions(+), 105 deletions(-) diff --git a/hub/site/bun.lockb b/hub/site/bun.lockb index 0111c6bbe70ebce3fae444f5b02ecb50d26c7f35..6588bdaef066e29c88151b3db2791e9c63195ef8 100755 GIT binary patch delta 27763 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 diff --git a/hub/site/package.json b/hub/site/package.json index 2b6defb..0c354ca 100644 --- a/hub/site/package.json +++ b/hub/site/package.json @@ -37,6 +37,7 @@ "recharts": "^2.13.0-alpha.4", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", + "use-is-in-viewport": "^1.0.9", "valibot": "^0.36.0" }, "devDependencies": { diff --git a/hub/site/src/components/charts/bandwidth-chart.tsx b/hub/site/src/components/charts/bandwidth-chart.tsx index bcf430f..9eda231 100644 --- a/hub/site/src/components/charts/bandwidth-chart.tsx +++ b/hub/site/src/components/charts/bandwidth-chart.tsx @@ -1,12 +1,12 @@ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' -import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' -import Spinner from '../spinner' +import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils' +// import Spinner from '../spinner' import { useStore } from '@nanostores/react' import { $chartTime } from '@/lib/stores' import { SystemStatsRecord } from '@/types' -import { useRef } from 'react' +import { useMemo, useRef } from 'react' export default function BandwidthChart({ ticks, @@ -19,13 +19,17 @@ export default function BandwidthChart({ const yAxisWidth = useYaxisWidth(chartRef) const chartTime = useStore($chartTime) - if (!systemData.length || !ticks.length) { - return - } + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) return (
- + {/* {!yAxisSet && } */} + diff --git a/hub/site/src/components/charts/container-cpu-chart.tsx b/hub/site/src/components/charts/container-cpu-chart.tsx index b162145..5e15c09 100644 --- a/hub/site/src/components/charts/container-cpu-chart.tsx +++ b/hub/site/src/components/charts/container-cpu-chart.tsx @@ -6,8 +6,8 @@ import { ChartTooltipContent, } from '@/components/ui/chart' import { useMemo, useRef } from 'react' -import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' -import Spinner from '../spinner' +import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils' +// import Spinner from '../spinner' import { useStore } from '@nanostores/react' import { $chartTime } from '@/lib/stores' @@ -22,6 +22,8 @@ export default function ContainerCpuChart({ const yAxisWidth = useYaxisWidth(chartRef) const chartTime = useStore($chartTime) + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) + const chartConfig = useMemo(() => { let config = {} as Record< string, @@ -57,13 +59,19 @@ export default function ContainerCpuChart({ return config satisfies ChartConfig }, [chartData]) - if (!chartData.length || !ticks.length) { - return - } + // if (!chartData.length || !ticks.length) { + // return + // } return (
- + {/* {!yAxisSet && } */} + [] ticks: number[] }) { + const chartTime = useStore($chartTime) const chartRef = useRef(null) const yAxisWidth = useYaxisWidth(chartRef) - const chartTime = useStore($chartTime) + + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) const chartConfig = useMemo(() => { let config = {} as Record< @@ -57,13 +59,19 @@ export default function ContainerMemChart({ return config satisfies ChartConfig }, [chartData]) - if (!chartData.length || !ticks.length) { - return - } + // if (!chartData.length || !ticks.length) { + // return + // } return (
- + {/* {!yAxisSet && } */} + ( [] ticks: number[] }) { + const chartTime = useStore($chartTime) const chartRef = useRef(null) const yAxisWidth = useYaxisWidth(chartRef) - const chartTime = useStore($chartTime) + + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) const chartConfig = useMemo(() => { let config = {} as Record< @@ -57,13 +59,19 @@ export default function ContainerCpuChart({ return config satisfies ChartConfig }, [chartData]) - if (!chartData.length || !ticks.length) { - return - } + // if (!chartData.length || !ticks.length) { + // return + // } return (
- + {/* {!yAxisSet && } */} + data?.[key]?.[2] ?? 0} type="monotoneX" fill={chartConfig[key].color} diff --git a/hub/site/src/components/charts/cpu-chart.tsx b/hub/site/src/components/charts/cpu-chart.tsx index 0d1f1bb..5edaf40 100644 --- a/hub/site/src/components/charts/cpu-chart.tsx +++ b/hub/site/src/components/charts/cpu-chart.tsx @@ -1,12 +1,12 @@ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' -import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' -import Spinner from '../spinner' +import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils' +// import Spinner from '../spinner' import { useStore } from '@nanostores/react' import { $chartTime } from '@/lib/stores' import { SystemStatsRecord } from '@/types' -import { useRef } from 'react' +import { useMemo, useRef } from 'react' export default function CpuChart({ ticks, @@ -15,17 +15,24 @@ export default function CpuChart({ ticks: number[] systemData: SystemStatsRecord[] }) { + const chartTime = useStore($chartTime) const chartRef = useRef(null) const yAxisWidth = useYaxisWidth(chartRef) - const chartTime = useStore($chartTime) - if (!systemData.length || !ticks.length) { - return - } + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) + + // if (!systemData.length || !ticks.length) { + // return + // } return (
- + diff --git a/hub/site/src/components/charts/disk-chart.tsx b/hub/site/src/components/charts/disk-chart.tsx index b488a02..430cff1 100644 --- a/hub/site/src/components/charts/disk-chart.tsx +++ b/hub/site/src/components/charts/disk-chart.tsx @@ -1,9 +1,9 @@ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' 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 Spinner from '../spinner' +// import Spinner from '../spinner' import { useStore } from '@nanostores/react' import { $chartTime } from '@/lib/stores' import { SystemStatsRecord } from '@/types' @@ -15,9 +15,11 @@ export default function DiskChart({ ticks: number[] systemData: SystemStatsRecord[] }) { + const chartTime = useStore($chartTime) const chartRef = useRef(null) const yAxisWidth = useYaxisWidth(chartRef) - const chartTime = useStore($chartTime) + + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) const diskSize = useMemo(() => { return Math.round(systemData[0]?.stats.d) @@ -32,13 +34,19 @@ export default function DiskChart({ // return ticks // }, [diskSize]) - if (!systemData.length || !ticks.length) { - return - } + // if (!systemData.length || !ticks.length) { + // return + // } return (
- + {/* {!yAxisSet && } */} + diff --git a/hub/site/src/components/charts/disk-io-chart.tsx b/hub/site/src/components/charts/disk-io-chart.tsx index 813f0f6..d13bb25 100644 --- a/hub/site/src/components/charts/disk-io-chart.tsx +++ b/hub/site/src/components/charts/disk-io-chart.tsx @@ -1,12 +1,12 @@ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart' -import { chartTimeData, formatShortDate, useYaxisWidth } from '@/lib/utils' -import Spinner from '../spinner' +import { chartTimeData, cn, formatShortDate, useYaxisWidth } from '@/lib/utils' +// import Spinner from '../spinner' import { useStore } from '@nanostores/react' import { $chartTime } from '@/lib/stores' import { SystemStatsRecord } from '@/types' -import { useRef } from 'react' +import { useMemo, useRef } from 'react' export default function DiskIoChart({ ticks, @@ -15,17 +15,25 @@ export default function DiskIoChart({ ticks: number[] systemData: SystemStatsRecord[] }) { + const chartTime = useStore($chartTime) const chartRef = useRef(null) const yAxisWidth = useYaxisWidth(chartRef) - const chartTime = useStore($chartTime) - if (!systemData.length || !ticks.length) { - return - } + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) + + // if (!systemData.length || !ticks.length) { + // return + // } return (
- + {/* {!yAxisSet && } */} + diff --git a/hub/site/src/components/charts/mem-chart.tsx b/hub/site/src/components/charts/mem-chart.tsx index 0dbc477..f9c229a 100644 --- a/hub/site/src/components/charts/mem-chart.tsx +++ b/hub/site/src/components/charts/mem-chart.tsx @@ -1,9 +1,9 @@ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' 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 Spinner from '../spinner' +// import Spinner from '../spinner' import { useStore } from '@nanostores/react' import { $chartTime } from '@/lib/stores' import { SystemStatsRecord } from '@/types' @@ -19,18 +19,26 @@ export default function MemChart({ const chartRef = useRef(null) const yAxisWidth = useYaxisWidth(chartRef) + const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth]) + const totalMem = useMemo(() => { const maxMem = Math.ceil(systemData[0]?.stats.m) return maxMem > 2 && maxMem % 2 !== 0 ? maxMem + 1 : maxMem }, [systemData]) - if (!systemData.length || !ticks.length) { - return - } + // if (!systemData.length || !ticks.length) { + // return + // } return (
- + {/* {!yAxisSet && } */} + diff --git a/hub/site/src/components/routes/system.tsx b/hub/site/src/components/routes/system.tsx index d5a84dc..77bae84 100644 --- a/hub/site/src/components/routes/system.tsx +++ b/hub/site/src/components/routes/system.tsx @@ -1,12 +1,12 @@ import { $updatedSystem, $systems, pb, $chartTime } from '@/lib/stores' 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 { useStore } from '@nanostores/react' import Spinner from '../spinner' import { ClockArrowUp, CpuIcon, GlobeIcon } from 'lucide-react' 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 { scaleTime } from 'd3-scale' 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 [server, setServer] = useState({} as SystemRecord) const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[]) - const [dockerCpuChartData, setDockerCpuChartData] = useState( - [] as Record[] - ) - const [dockerMemChartData, setDockerMemChartData] = useState( - [] as Record[] - ) - const [dockerNetChartData, setDockerNetChartData] = useState( - [] as Record[] - ) + const [dockerCpuChartData, setDockerCpuChartData] = useState[]>() + const [dockerMemChartData, setDockerMemChartData] = useState[]>() + const [dockerNetChartData, setDockerNetChartData] = + useState[]>() useEffect(() => { document.title = `${name} / Beszel` @@ -47,9 +42,9 @@ export default function ServerDetail({ name }: { name: string }) { const resetCharts = useCallback(() => { setSystemStats([]) - setDockerCpuChartData([]) - setDockerMemChartData([]) - setDockerNetChartData([]) + setDockerCpuChartData(undefined) + setDockerMemChartData(undefined) + setDockerNetChartData(undefined) }, []) useEffect(resetCharts, [chartTime]) @@ -121,7 +116,9 @@ export default function ServerDetail({ name }: { name: string }) { sort: 'created', }) .then((records) => { - makeContainerData(records) + if (records.length) { + makeContainerData(records) + } // setContainers(records) }) }, [server, chartTime]) @@ -129,15 +126,14 @@ export default function ServerDetail({ name }: { name: string }) { // container stats for charts const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => { // console.log('containers', containers) - const dockerCpuData = [] as typeof dockerCpuChartData - const dockerMemData = [] as typeof dockerMemChartData - const dockerNetData = [] as typeof dockerNetChartData - + const dockerCpuData = [] + const dockerMemData = [] + const dockerNetData = [] for (let { created, stats } of containers) { const time = new Date(created).getTime() - let cpuData = { time } as (typeof dockerCpuChartData)[0] - let memData = { time } as (typeof dockerMemChartData)[0] - let netData = { time } as (typeof dockerNetChartData)[0] + let cpuData = { time } as Record + let memData = { time } as Record + let netData = { time } as Record for (let container of stats) { cpuData[container.n] = container.c memData[container.n] = container.m @@ -225,7 +221,7 @@ export default function ServerDetail({ name }: { name: string }) { - {dockerCpuChartData.length > 0 && ( + {dockerCpuChartData && ( @@ -235,7 +231,7 @@ export default function ServerDetail({ name }: { name: string }) { - {dockerMemChartData.length > 0 && ( + {dockerMemChartData && ( @@ -256,7 +252,7 @@ export default function ServerDetail({ name }: { name: string }) { - {dockerNetChartData.length > 0 && ( + {dockerNetChartData && ( (null) + const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target }) return ( - + {title} {description} @@ -287,7 +285,8 @@ function ChartCard({
- }>{children} + {} + {isInViewport && {children}} ) diff --git a/hub/site/src/lib/utils.ts b/hub/site/src/lib/utils.ts index 50e728e..6187cb4 100644 --- a/hub/site/src/lib/utils.ts +++ b/hub/site/src/lib/utils.ts @@ -7,6 +7,7 @@ import { RecordModel, RecordSubscription } from 'pocketbase' import { WritableAtom } from 'nanostores' import { timeDay, timeHour } from 'd3-time' import { useEffect, useState } from 'react' +import useIsInViewport, { CallbackRef, HookOptions } from 'use-is-in-viewport' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -183,17 +184,36 @@ export const chartTimeData: ChartTimeData = { /** Hacky solution to set the correct width of the yAxis in recharts */ export function useYaxisWidth(chartRef: React.RefObject) { - const [yAxisWidth, setYAxisWidth] = useState(90) + const [yAxisWidth, setYAxisWidth] = useState(180) useEffect(() => { let interval = setInterval(() => { // console.log('chartRef', chartRef.current) const yAxisElement = chartRef?.current?.querySelector('.yAxis') if (yAxisElement) { - console.log('yAxisElement', yAxisElement) + // console.log('yAxisElement', yAxisElement) setYAxisWidth(yAxisElement.getBoundingClientRect().width + 22) clearInterval(interval) } - }, 16) + }, 0) + return () => clearInterval(interval) }, []) 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] +}