mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
lazy load charts and disable chart animations
This commit is contained in:
Binary file not shown.
@@ -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": {
|
||||||
|
@@ -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>
|
||||||
|
@@ -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}
|
||||||
|
@@ -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}
|
||||||
|
@@ -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}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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) => {
|
||||||
makeContainerData(records)
|
if (records.length) {
|
||||||
|
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>
|
||||||
)
|
)
|
||||||
|
@@ -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]
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user