From 82888684d986311e5b76a8e0d890c82856ca2f1c Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Thu, 11 Jul 2024 00:38:30 -0400 Subject: [PATCH] updates --- .../components/charts/container-cpu-chart.tsx | 112 ++++ site/src/components/charts/cpu-chart.tsx | 72 +++ site/src/components/charts/disk-chart.tsx | 84 +++ site/src/components/charts/mem-chart.tsx | 72 +++ site/src/components/cpu-chart.tsx | 117 ---- site/src/components/routes/server.tsx | 138 ++++- site/src/components/ui/chart.tsx | 562 ++++++++---------- site/src/lib/utils.ts | 13 +- site/src/main.tsx | 2 - site/src/types.d.ts | 7 +- 10 files changed, 742 insertions(+), 437 deletions(-) create mode 100644 site/src/components/charts/container-cpu-chart.tsx create mode 100644 site/src/components/charts/cpu-chart.tsx create mode 100644 site/src/components/charts/disk-chart.tsx create mode 100644 site/src/components/charts/mem-chart.tsx delete mode 100644 site/src/components/cpu-chart.tsx diff --git a/site/src/components/charts/container-cpu-chart.tsx b/site/src/components/charts/container-cpu-chart.tsx new file mode 100644 index 0000000..9a0162f --- /dev/null +++ b/site/src/components/charts/container-cpu-chart.tsx @@ -0,0 +1,112 @@ +'use client' + +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' +import { + ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart' +import { useMemo, useState } from 'react' +import { formatShortDate, formatShortTime } from '@/lib/utils' + +export default function ({ chartData }: { chartData: Record[] }) { + const [containerNames, setContainerNames] = useState([] as string[]) + + const chartConfig = useMemo(() => { + console.log('chartData', chartData) + let config = {} as Record< + string, + { + label: string + color: string + } + > + const lastRecord = chartData.at(-1) + // @ts-ignore + let allKeys = new Set(Object.keys(lastRecord)) + allKeys.delete('time') + const keys = Array.from(allKeys) + keys.sort((a, b) => (lastRecord![b] as number) - (lastRecord![a] as number)) + setContainerNames(keys) + const length = keys.length + for (let i = 0; i < length; i++) { + const key = keys[i] + const hue = ((i * 360) / length) % 360 + config[key] = { + label: key, + color: `hsl(${hue}, 60%, 60%)`, + } + } + console.log('config', config) + return config satisfies ChartConfig + }, [chartData]) + + if (!containerNames.length) { + return null + } + + return ( + + + + {/* */} + + { + // console.log('itemSorter', item) + // return -item.value + // }} + content={ + { + // console.log('itemSorter', item) + // return -item.value + // }} + indicator="line" + /> + } + /> + {containerNames.map((key) => ( + + ))} + {/* */} + {/* } className="flex-wrap gap-y-2 mb-2" /> */} + + + ) +} diff --git a/site/src/components/charts/cpu-chart.tsx b/site/src/components/charts/cpu-chart.tsx new file mode 100644 index 0000000..b5e3e9c --- /dev/null +++ b/site/src/components/charts/cpu-chart.tsx @@ -0,0 +1,72 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' + +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart' +import { formatShortDate, formatShortTime } from '@/lib/utils' +// for (const data of chartData) { +// data.month = formatDateShort(data.month) +// } + +const chartConfig = { + cpu: { + label: 'CPU Usage', + color: 'hsl(var(--chart-1))', + }, +} satisfies ChartConfig + +export default function ({ chartData }: { chartData: { time: string; cpu: number }[] }) { + return ( + + + + `${v}%`} + /> + {/* todo: short time if first date is same day, otherwise short date */} + + + } + /> + + + + ) +} diff --git a/site/src/components/charts/disk-chart.tsx b/site/src/components/charts/disk-chart.tsx new file mode 100644 index 0000000..bf405d7 --- /dev/null +++ b/site/src/components/charts/disk-chart.tsx @@ -0,0 +1,84 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' + +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart' +import { formatShortDate, formatShortTime } from '@/lib/utils' +import { useMemo } from 'react' +// for (const data of chartData) { +// data.month = formatDateShort(data.month) +// } + +const chartConfig = { + diskUsed: { + label: 'Disk Use', + color: 'hsl(var(--chart-3))', + }, +} satisfies ChartConfig + +export default function ({ + chartData, +}: { + chartData: { time: string; disk: number; diskUsed: number }[] +}) { + const diskSize = useMemo(() => { + return Math.round(chartData[0]?.disk) + }, [chartData]) + + // const ticks = useMemo(() => { + // let ticks = [0] + // for (let i = 1; i < diskSize; i += diskSize / 5) { + // ticks.push(Math.trunc(i)) + // } + // ticks.push(diskSize) + // return ticks + // }, [diskSize]) + + return ( + + + + `${v} GiB`} + /> + {/* todo: short time if first date is same day, otherwise short date */} + + } + /> + + + + ) +} diff --git a/site/src/components/charts/mem-chart.tsx b/site/src/components/charts/mem-chart.tsx new file mode 100644 index 0000000..699dc02 --- /dev/null +++ b/site/src/components/charts/mem-chart.tsx @@ -0,0 +1,72 @@ +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' + +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart' +import { formatShortDate, formatShortTime } from '@/lib/utils' +import { useMemo } from 'react' + +const chartConfig = { + memUsed: { + label: 'Memory Use', + color: 'hsl(var(--chart-2))', + }, +} satisfies ChartConfig + +export default function ({ + chartData, +}: { + chartData: { time: string; mem: number; memUsed: number }[] +}) { + const totalMem = useMemo(() => { + return Math.ceil(chartData[0]?.mem) + }, [chartData]) + + return ( + + + + `${v} GiB`} + /> + {/* todo: short time if first date is same day, otherwise short date */} + + } + /> + + + + ) +} diff --git a/site/src/components/cpu-chart.tsx b/site/src/components/cpu-chart.tsx deleted file mode 100644 index cb66075..0000000 --- a/site/src/components/cpu-chart.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' - -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from '@/components/ui/chart' -import { formatDateShort } from '@/lib/utils' - -const chartData = [ - { month: '2024-07-09 23:29:08.976Z', cpu: 6.2 }, - { month: '2024-07-09 23:28:08.976Z', cpu: 2.8 }, - { month: '2024-07-09 23:27:08.976Z', cpu: 9.5 }, - { month: '2024-07-09 23:26:08.976Z', cpu: 23.4 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:29:08.976Z', cpu: 6.2 }, - { month: '2024-07-09 23:28:08.976Z', cpu: 2.8 }, - { month: '2024-07-09 23:27:08.976Z', cpu: 9.5 }, - { month: '2024-07-09 23:26:08.976Z', cpu: 23.4 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:29:08.976Z', cpu: 6.2 }, - { month: '2024-07-09 23:28:08.976Z', cpu: 2.8 }, - { month: '2024-07-09 23:27:08.976Z', cpu: 9.5 }, - { month: '2024-07-09 23:26:08.976Z', cpu: 23.4 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:29:08.976Z', cpu: 6.2 }, - { month: '2024-07-09 23:28:08.976Z', cpu: 2.8 }, - { month: '2024-07-09 23:27:08.976Z', cpu: 9.5 }, - { month: '2024-07-09 23:26:08.976Z', cpu: 23.4 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:29:08.976Z', cpu: 6.2 }, - { month: '2024-07-09 23:28:08.976Z', cpu: 2.8 }, - { month: '2024-07-09 23:27:08.976Z', cpu: 9.5 }, - { month: '2024-07-09 23:26:08.976Z', cpu: 23.4 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:29:08.976Z', cpu: 6.2 }, - { month: '2024-07-09 23:28:08.976Z', cpu: 2.8 }, - { month: '2024-07-09 23:27:08.976Z', cpu: 9.5 }, - { month: '2024-07-09 23:26:08.976Z', cpu: 23.4 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:29:08.976Z', cpu: 6.2 }, - { month: '2024-07-09 23:28:08.976Z', cpu: 2.8 }, - { month: '2024-07-09 23:27:08.976Z', cpu: 9.5 }, - { month: '2024-07-09 23:26:08.976Z', cpu: 23.4 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, - { month: '2024-07-09 23:29:08.976Z', cpu: 6.2 }, - { month: '2024-07-09 23:28:08.976Z', cpu: 2.8 }, - { month: '2024-07-09 23:27:08.976Z', cpu: 9.5 }, - { month: '2024-07-09 23:26:08.976Z', cpu: 23.4 }, - { month: '2024-07-09 23:25:08.976Z', cpu: 4.3 }, - { month: '2024-07-09 23:24:08.976Z', cpu: 9.1 }, -] - -// for (const data of chartData) { -// data.month = formatDateShort(data.month) -// } - -const chartConfig = { - cpu: { - label: 'cpu', - color: 'hsl(var(--chart-1))', - }, -} satisfies ChartConfig - -export default function () { - return ( - - - - - formatDateShort(value)} - /> - } /> - - - - ) -} diff --git a/site/src/components/routes/server.tsx b/site/src/components/routes/server.tsx index 7cc0e20..82bb280 100644 --- a/site/src/components/routes/server.tsx +++ b/site/src/components/routes/server.tsx @@ -1,12 +1,15 @@ import { $servers, pb } from '@/lib/stores' -import { ContainerStatsRecord, SystemRecord } from '@/types' +import { ContainerStatsRecord, SystemRecord, SystemStats, SystemStatsRecord } from '@/types' import { Suspense, lazy, useEffect, useState } from 'react' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card' import { useStore } from '@nanostores/react' import Spinner from '../spinner' -// import { CpuChart } from '../cpu-chart' +import CpuChart from '../charts/cpu-chart' +import MemChart from '../charts/mem-chart' +import DiskChart from '../charts/disk-chart' +import ContainerCpuChart from '../charts/container-cpu-chart' -const CpuChart = lazy(() => import('../cpu-chart')) +// const CpuChart = lazy(() => import('../cpu-chart')) function timestampToBrowserTime(timestamp: string) { const date = new Date(timestamp) @@ -25,9 +28,57 @@ export default function ServerDetail({ name }: { name: string }) { const [server, setServer] = useState({} as SystemRecord) const [containers, setContainers] = useState([] as ContainerStatsRecord[]) + const [serverStats, setServerStats] = useState([] as SystemStatsRecord[]) + const [cpuChartData, setCpuChartData] = useState({} as { time: string; cpu: number }[]) + const [memChartData, setMemChartData] = useState( + {} as { time: string; mem: number; memUsed: number }[] + ) + const [diskChartData, setDiskChartData] = useState( + {} as { time: string; disk: number; diskUsed: number }[] + ) + const [containerCpuChartData, setContainerCpuChartData] = useState( + [] as Record[] + ) + + // get stats + useEffect(() => { + if (!('name' in server)) { + return + } + + pb.collection('system_stats') + .getList(1, 60, { + filter: `system="${server.id}"`, + fields: 'created,stats', + sort: '-created', + }) + .then((records) => { + console.log('stats', records) + setServerStats(records.items) + }) + }, [server]) + + // get cpu data + useEffect(() => { + if (!serverStats.length) { + return + } + const cpuData = [] as { time: string; cpu: number }[] + const memData = [] as { time: string; mem: number; memUsed: number }[] + const diskData = [] as { time: string; disk: number; diskUsed: number }[] + for (let { created, stats } of serverStats) { + cpuData.push({ time: created, cpu: stats.cpu }) + memData.push({ time: created, mem: stats.mem, memUsed: stats.memUsed }) + diskData.push({ time: created, disk: stats.disk, diskUsed: stats.diskUsed }) + } + setCpuChartData(cpuData.reverse()) + setMemChartData(memData.reverse()) + setDiskChartData(diskData.reverse()) + }, [serverStats]) + useEffect(() => { document.title = name - }, []) + }, [name]) useEffect(() => { if ($servers.get().length === 0) { @@ -47,29 +98,84 @@ export default function ServerDetail({ name }: { name: string }) { // }) pb.collection('container_stats') - .getList(1, 2, { + .getList(1, 60, { filter: `system="${matchingServer.id}"`, fields: 'created,stats', sort: '-created', }) .then((records) => { - console.log('records', records) + // console.log('records', records) setContainers(records.items) }) - }, [servers]) + }, [servers, name]) + + // container stats for charts + useEffect(() => { + console.log('containers', containers) + const containerCpuData = [] as Record[] + + for (let { created, stats } of containers) { + let obj = { time: created } as Record + for (let { name, cpu } of stats) { + obj[name] = cpu * 10 + } + containerCpuData.push(obj) + } + setContainerCpuChartData(containerCpuData.reverse()) + console.log('containerCpuData', containerCpuData) + }, [containers]) return ( <> -
- +
+ CPU Usage - Showing total visitors for the last 30 minutes + + Average usage of the one minute preceding the recorded time + - - }> - - + + {/* }> */} + + {/* */} + + + + + Memory Usage + Precise usage at the recorded time + + + {/* }> */} + + {/* */} + + + + + Disk Usage + Precise usage at the recorded time + + + {/* }> */} + + {/* */} + + + + + Container CPU Usage + + Average usage of the one minute preceding the recorded time + + + + {/* }> */} + {containerCpuChartData.length > 0 && ( + + )} + {/* */}
@@ -86,14 +192,14 @@ export default function ServerDetail({ name }: { name: string }) {
- + {/* Containers
{JSON.stringify(containers, null, 2)}
-
+
*/} ) } diff --git a/site/src/components/ui/chart.tsx b/site/src/components/ui/chart.tsx index 1b7cbc8..e1b7d4f 100644 --- a/site/src/components/ui/chart.tsx +++ b/site/src/components/ui/chart.tsx @@ -1,361 +1,325 @@ -import * as React from "react" -import * as RechartsPrimitive from "recharts" +import * as React from 'react' +import * as RechartsPrimitive from 'recharts' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' // Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const +const THEMES = { light: '', dark: '.dark' } as const export type ChartConfig = { - [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } - ) + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) } type ChartContextProps = { - config: ChartConfig + config: ChartConfig } const ChartContext = React.createContext(null) function useChart() { - const context = React.useContext(ChartContext) + const context = React.useContext(ChartContext) - if (!context) { - throw new Error("useChart must be used within a ") - } + if (!context) { + throw new Error('useChart must be used within a ') + } - return context + return context } const ChartContainer = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - config: ChartConfig - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"] - } + HTMLDivElement, + React.ComponentProps<'div'> & { + config: ChartConfig + children: React.ComponentProps['children'] + } >(({ id, className, children, config, ...props }, ref) => { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, '')}` - return ( - -
- - - {children} - -
-
- ) + return ( + +
+ + {children} +
+
+ ) }) -ChartContainer.displayName = "Chart" +ChartContainer.displayName = 'Chart' const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([_, config]) => config.theme || config.color - ) + const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color) - if (!colorConfig.length) { - return null - } + if (!colorConfig.length) { + return null + } - return ( -