From 1b0dffc1ab03be87ff061431c875ee08e3c77107 Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Mon, 14 Oct 2024 17:25:21 -0400 Subject: [PATCH] combine docker charts and chart data --- .../site/src/components/charts/area-chart.tsx | 19 +- .../src/components/charts/container-chart.tsx | 71 +++++-- .../components/charts/container-net-chart.tsx | 167 --------------- .../site/src/components/charts/disk-chart.tsx | 37 ++-- .../site/src/components/charts/mem-chart.tsx | 25 +-- .../site/src/components/charts/swap-chart.tsx | 24 +-- .../components/charts/temperature-chart.tsx | 31 +-- beszel/site/src/components/routes/system.tsx | 199 +++++++++--------- beszel/site/src/lib/utils.ts | 23 +- beszel/site/src/types.d.ts | 14 ++ 10 files changed, 232 insertions(+), 378 deletions(-) delete mode 100644 beszel/site/src/components/charts/container-net-chart.tsx diff --git a/beszel/site/src/components/charts/area-chart.tsx b/beszel/site/src/components/charts/area-chart.tsx index 6eaa14f..5e78f5e 100644 --- a/beszel/site/src/components/charts/area-chart.tsx +++ b/beszel/site/src/components/charts/area-chart.tsx @@ -11,7 +11,7 @@ import { chartMargin, } from '@/lib/utils' // import Spinner from '../spinner' -import { ChartTimes, SystemStatsRecord } from '@/types' +import { ChartData } from '@/types' import { memo, useMemo } from 'react' /** [label, key, color, opacity] */ @@ -31,21 +31,16 @@ export default memo(function AreaChartDefault({ maxToggled = false, unit = ' MB/s', chartName, - systemChartData, + chartData, }: { maxToggled?: boolean unit?: string chartName: string - systemChartData: { - systemStats: SystemStatsRecord[] - ticks: number[] - domain: number[] - chartTime: ChartTimes - } + chartData: ChartData }) { const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() - const { chartTime } = systemChartData + const { chartTime } = chartData const showMax = chartTime !== '1h' && maxToggled @@ -81,7 +76,7 @@ export default memo(function AreaChartDefault({ 'opacity-100': yAxisWidth, })} > - + [] - ticks: number[] - domain: number[] - chartTime: ChartTimes - } }) { const filter = useStore($containerFilter) const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() - const { containerData, ticks, domain, chartTime } = containerChartData + const { containerData, ticks, domain, chartTime } = chartData + + const isNetChart = chartName === 'net' const chartConfig = useMemo(() => { let config = {} as Record< @@ -50,14 +52,18 @@ export default memo(function ContainerCpuChart({ const totalUsage = {} as Record for (let stats of containerData) { for (let key in stats) { - if (!key || typeof stats[key] === 'number') { + if (!key || key === 'created') { continue } if (!(key in totalUsage)) { totalUsage[key] = 0 } - // @ts-ignore - totalUsage[key] += stats[key]?.[dataKey] ?? 0 + if (isNetChart) { + totalUsage[key] += stats[key]?.ns ?? 0 + stats[key]?.nr ?? 0 + } else { + // @ts-ignore + totalUsage[key] += stats[key]?.[dataKey] ?? 0 + } } } let keys = Object.keys(totalUsage) @@ -72,7 +78,7 @@ export default memo(function ContainerCpuChart({ } } return config satisfies ChartConfig - }, [containerChartData]) + }, [chartData]) // console.log('rendered at', new Date()) @@ -95,8 +101,12 @@ export default memo(function ContainerCpuChart({ className="tracking-tighter" width={yAxisWidth} tickFormatter={(value) => { - const val = toFixedWithoutTrailingZeros(value, 2) + unit - return updateYAxisWidth(val) + if (chartName === 'cpu') { + const val = toFixedWithoutTrailingZeros(value, 2) + unit + return updateYAxisWidth(val) + } + const { v, u } = getSizeAndUnit(value, false) + return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isNetChart ? '/s' : ''}`) }} tickLine={false} axisLine={false} @@ -122,8 +132,26 @@ export default memo(function ContainerCpuChart({ content={ decimalString(item.value) + unit} - // indicator="line" + contentFormatter={(item, key) => { + if (!isNetChart) { + return decimalString(item.value) + unit + } + try { + const sent = item?.payload?.[key]?.ns ?? 0 + const received = item?.payload?.[key]?.nr ?? 0 + return ( + + {decimalString(received)} MB/s + rx + + {decimalString(sent)} MB/s + tx + + ) + } catch (e) { + return null + } + }} /> } /> @@ -135,7 +163,12 @@ export default memo(function ContainerCpuChart({ { + if (isNetChart) { + return data[key]?.ns ?? 0 + data?.[key]?.nr ?? 0 + } + return data[key]?.[dataKey] ?? 0 + }} name={key} type="monotoneX" fill={chartConfig[key].color} diff --git a/beszel/site/src/components/charts/container-net-chart.tsx b/beszel/site/src/components/charts/container-net-chart.tsx deleted file mode 100644 index 00ae790..0000000 --- a/beszel/site/src/components/charts/container-net-chart.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from '@/components/ui/chart' -import { memo, useMemo } from 'react' -import { - useYAxisWidth, - chartTimeData, - cn, - formatShortDate, - toFixedWithoutTrailingZeros, - decimalString, - chartMargin, -} from '@/lib/utils' -import { useStore } from '@nanostores/react' -import { $containerFilter } from '@/lib/stores' -import { Separator } from '@/components/ui/separator' -import { ChartTimes, ContainerStats } from '@/types' - -export default memo(function ContainerCpuChart({ - containerChartData, -}: { - containerChartData: { - containerData: Record[] - ticks: number[] - domain: number[] - chartTime: ChartTimes - } -}) { - const filter = useStore($containerFilter) - const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() - - const { containerData, ticks, domain, chartTime } = containerChartData - - const chartConfig = useMemo(() => { - let config = {} as Record< - string, - { - label: string - color: string - } - > - const totalUsage = {} as Record - for (let stats of containerData) { - for (let key in stats) { - // continue if number and not container stats - if (!key || typeof stats[key] === 'number') { - continue - } - if (!(key in totalUsage)) { - totalUsage[key] = 0 - } - totalUsage[key] += stats[key]?.ns ?? 0 + stats[key]?.nr ?? 0 - } - } - let keys = Object.keys(totalUsage) - keys.sort((a, b) => (totalUsage[a] > totalUsage[b] ? -1 : 1)) - 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%, 55%)`, - } - } - return config satisfies ChartConfig - }, [containerChartData]) - - // console.log('rendered at', new Date()) - - return ( -
- {/* {!yAxisSet && } */} - - - - Math.max(Math.ceil(max), 0.4)]} - width={yAxisWidth} - tickLine={false} - axisLine={false} - tickFormatter={(value) => { - const val = toFixedWithoutTrailingZeros(value, 2) + ' MB/s' - return updateYAxisWidth(val) - }} - /> - - formatShortDate(data[0].payload.created)} - // @ts-ignore - itemSorter={(a, b) => b.value - a.value} - content={ - { - try { - const sent = item?.payload?.[key]?.ns ?? 0 - const received = item?.payload?.[key]?.nr ?? 0 - return ( - - {decimalString(received)} MB/s - rx - - {decimalString(sent)} MB/s tx - - ) - } catch (e) { - return null - } - }} - /> - } - /> - {Object.keys(chartConfig).map((key) => { - const filtered = filter && !key.includes(filter) - let fillOpacity = filtered ? 0.05 : 0.4 - let strokeOpacity = filtered ? 0.1 : 1 - return ( - data?.[key]?.ns ?? 0 + data?.[key]?.nr ?? 0} - type="monotoneX" - fill={chartConfig[key].color} - fillOpacity={fillOpacity} - stroke={chartConfig[key].color} - strokeOpacity={strokeOpacity} - activeDot={{ opacity: filtered ? 0 : 1 }} - stackId="a" - /> - ) - })} - - -
- ) -}) diff --git a/beszel/site/src/components/charts/disk-chart.tsx b/beszel/site/src/components/charts/disk-chart.tsx index abcd5f0..fd711ac 100644 --- a/beszel/site/src/components/charts/disk-chart.tsx +++ b/beszel/site/src/components/charts/disk-chart.tsx @@ -8,26 +8,20 @@ import { formatShortDate, decimalString, toFixedFloat, - getSizeVal, - getSizeUnit, chartMargin, + getSizeAndUnit, } from '@/lib/utils' -import { ChartTimes, SystemStatsRecord } from '@/types' +import { ChartData } from '@/types' import { memo } from 'react' export default memo(function DiskChart({ dataKey, diskSize, - systemChartData, + chartData, }: { dataKey: string diskSize: number - systemChartData: { - systemStats: SystemStatsRecord[] - ticks: number[] - domain: number[] - chartTime: ChartTimes - } + chartData: ChartData }) { const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() @@ -40,7 +34,7 @@ export default memo(function DiskChart({ 'opacity-100': yAxisWidth, })} > - + - updateYAxisWidth(toFixedFloat(getSizeVal(value), 2) + getSizeUnit(value)) - } + tickFormatter={(value) => { + const { v, u } = getSizeAndUnit(value) + return updateYAxisWidth(toFixedFloat(v, 2) + u) + }} /> formatShortDate(data[0].payload.created)} - contentFormatter={({ value }) => - decimalString(getSizeVal(value)) + getSizeUnit(value) - } - // indicator="line" + contentFormatter={({ value }) => { + const { v, u } = getSizeAndUnit(value) + return decimalString(v) + u + }} /> } /> diff --git a/beszel/site/src/components/charts/mem-chart.tsx b/beszel/site/src/components/charts/mem-chart.tsx index 097de43..2b6e916 100644 --- a/beszel/site/src/components/charts/mem-chart.tsx +++ b/beszel/site/src/components/charts/mem-chart.tsx @@ -11,21 +11,12 @@ import { chartMargin, } from '@/lib/utils' import { memo } from 'react' -import { ChartTimes, SystemStatsRecord } from '@/types' +import { ChartData } from '@/types' -export default memo(function MemChart({ - systemChartData, -}: { - systemChartData: { - systemStats: SystemStatsRecord[] - ticks: number[] - domain: number[] - chartTime: ChartTimes - } -}) { +export default memo(function MemChart({ chartData }: { chartData: ChartData }) { const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() - const totalMem = toFixedFloat(systemChartData.systemStats.at(-1)?.stats.m ?? 0, 1) + const totalMem = toFixedFloat(chartData.systemStats.at(-1)?.stats.m ?? 0, 1) // console.log('rendered at', new Date()) @@ -37,7 +28,7 @@ export default memo(function MemChart({ 'opacity-100': yAxisWidth, })} > - + {totalMem && ( - {systemChartData.systemStats.at(-1)?.stats.mz && ( + {chartData.systemStats.at(-1)?.stats.mz && ( - + - toFixedWithoutTrailingZeros(systemChartData.systemStats.at(-1)?.stats.s ?? 0.04, 2), + () => toFixedWithoutTrailingZeros(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2), ]} width={yAxisWidth} tickLine={false} @@ -48,15 +38,15 @@ export default memo(function SwapChart({ /> { - const chartData = { data: [], colors: {} } as { + const newChartData = { data: [], colors: {} } as { data: Record[] colors: Record } const tempSums = {} as Record - for (let data of systemChartData.systemStats) { + for (let data of chartData.systemStats) { let newData = { created: data.created } as Record let keys = Object.keys(data.stats?.t ?? {}) for (let i = 0; i < keys.length; i++) { @@ -46,14 +37,14 @@ export default memo(function TemperatureChart({ newData[key] = data.stats.t![key] tempSums[key] = (tempSums[key] ?? 0) + newData[key] } - chartData.data.push(newData) + newChartData.data.push(newData) } const keys = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a]) for (let key of keys) { - chartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)` + newChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)` } - return chartData - }, [systemChartData]) + return newChartData + }, [chartData]) const colors = Object.keys(newChartData.colors) @@ -81,15 +72,15 @@ export default memo(function TemperatureChart({ /> import('../charts/area-chart')) -const ContainerChartDefault = lazy(() => import('../charts/container-chart')) +const ContainerChart = lazy(() => import('../charts/container-chart')) const MemChart = lazy(() => import('../charts/mem-chart')) const DiskChart = lazy(() => import('../charts/disk-chart')) -const ContainerNetChart = lazy(() => import('../charts/container-net-chart')) const SwapChart = lazy(() => import('../charts/swap-chart')) const TemperatureChart = lazy(() => import('../charts/temperature-chart')) -const cache = new Map() +const cache = new Map() + +// create ticks and domain for charts +function getTimeData(chartTime: ChartTimes, lastCreated: number) { + const cached = cache.get('td') + if (cached && cached.chartTime === chartTime) { + if (!lastCreated || cached.time >= lastCreated) { + return cached.data + } + } + + const now = new Date() + const startTime = chartTimeData[chartTime].getOffset(now) + const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => + date.getTime() + ) + const data = { + ticks, + domain: [chartTimeData[chartTime].getOffset(now).getTime(), now.getTime()], + } + cache.set('td', { time: now.getTime(), data, chartTime }) + return data +} + +// add empty values between records to make gaps if interval is too large +function addEmptyValues( + records: T[], + expectedInterval: number +) { + const modifiedRecords: T[] = [] + let prevTime = 0 + for (let i = 0; i < records.length; i++) { + const record = records[i] + record.created = new Date(record.created).getTime() + if (prevTime) { + const interval = record.created - prevTime + // if interval is too large, add a null record + if (interval > expectedInterval / 2 + expectedInterval) { + // @ts-ignore + modifiedRecords.push({ created: null, stats: null }) + } + } + prevTime = record.created + modifiedRecords.push(record) + } + return modifiedRecords +} + +async function getStats( + collection: string, + system: SystemRecord, + chartTime: ChartTimes +): Promise { + const lastCached = cache.get(`${system.id}_${chartTime}_${collection}`)?.at(-1)?.created as number + return await pb.collection(collection).getFullList({ + filter: pb.filter('system={:id} && created > {:created} && type={:type}', { + id: system.id, + created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined), + type: chartTimeData[chartTime].type, + }), + fields: 'created,stats', + sort: 'created', + }) +} export default function SystemDetail({ name }: { name: string }) { const systems = useStore($systems) @@ -36,9 +104,7 @@ export default function SystemDetail({ name }: { name: string }) { const [grid, setGrid] = useLocalStorage('grid', true) const [system, setSystem] = useState({} as SystemRecord) const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[]) - const [containerData, setContainerData] = useState( - [] as Record[] - ) + const [containerData, setContainerData] = useState([] as ChartData['containerData']) const netCardRef = useRef(null) const [containerFilterBar, setContainerFilterBar] = useState(null as null | JSX.Element) const isLongerChart = chartTime !== '1h' @@ -89,70 +155,18 @@ export default function SystemDetail({ name }: { name: string }) { } }, [system]) - function getTimeData() { - const now = new Date() - const startTime = chartTimeData[chartTime].getOffset(now) - const scale = scaleTime([startTime.getTime(), now], [0, systemStats.length]) - const ticks = scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime()) - - return { - ticks, - chartTime, - domain: [chartTimeData[chartTime].getOffset(now).getTime(), now.getTime()], - } - } - - const systemChartData = useMemo(() => { + const chartData: ChartData = useMemo(() => { + const lastCreated = Math.max( + (systemStats.at(-1)?.created as number) ?? 0, + (containerData.at(-1)?.created as number) ?? 0 + ) return { systemStats, - ...getTimeData(), - } - }, [systemStats]) - - const containerChartData = useMemo(() => { - return { containerData, - ...getTimeData(), + chartTime, + ...getTimeData(chartTime, lastCreated), } - }, [containerData]) - - async function getStats(collection: string): Promise { - const lastCached = cache.get(`${system.id}_${chartTime}_${collection}`)?.at(-1) - ?.created as number - return await pb.collection(collection).getFullList({ - filter: pb.filter('system={:id} && created > {:created} && type={:type}', { - id: system.id, - created: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined), - type: chartTimeData[chartTime].type, - }), - fields: 'created,stats', - sort: 'created', - }) - } - - // add empty values between records to make gaps if interval is too large - function addEmptyValues( - records: T[], - expectedInterval: number - ) { - const modifiedRecords: T[] = [] - let prevTime = 0 - for (let i = 0; i < records.length; i++) { - const record = records[i] - record.created = new Date(record.created).getTime() - if (prevTime) { - const interval = record.created - prevTime - // if interval is too large, add a null record - if (interval > expectedInterval / 2 + expectedInterval) { - // @ts-ignore - modifiedRecords.push({ created: null, stats: null }) - } - } - prevTime = record.created - modifiedRecords.push(record) - } - return modifiedRecords - } + }, [systemStats, containerData]) // get stats useEffect(() => { @@ -160,8 +174,8 @@ export default function SystemDetail({ name }: { name: string }) { return } Promise.allSettled([ - getStats('system_stats'), - getStats('container_stats'), + getStats('system_stats', system, chartTime), + getStats('container_stats', system, chartTime), ]).then(([systemStats, containerStats]) => { const { expectedInterval } = chartTimeData[chartTime] // make new system stats @@ -196,16 +210,16 @@ export default function SystemDetail({ name }: { name: string }) { // make container stats for charts const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => { - // console.log('containers', containers) - const containerData = [] as Record[] + const containerData = [] as ChartData['containerData'] for (let { created, stats } of containers) { if (!created) { - let nullData = { created: null } as unknown - containerData.push(nullData as Record) + // @ts-ignore add null value for gaps + containerData.push({ created: null }) continue } created = new Date(created).getTime() - let containerStats = { created } as Record + // @ts-ignore not dealing with this rn + let containerStats: ChartData['containerData'][0] = { created } for (let container of stats) { containerStats[container.n] = container } @@ -329,10 +343,9 @@ export default function SystemDetail({ name }: { name: string }) {