diff --git a/hub/site/bun.lockb b/hub/site/bun.lockb index 0111c6b..6588bda 100755 Binary files a/hub/site/bun.lockb and b/hub/site/bun.lockb differ 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] +}