Files
beszel-ipv6/internal/site/src/components/charts/hooks.ts
2026-03-27 12:26:00 -04:00

169 lines
5.7 KiB
TypeScript

import { useMemo, useState } from "react"
import { useStore } from "@nanostores/react"
import type { ChartConfig } from "@/components/ui/chart"
import type { ChartData, SystemStats, SystemStatsRecord } from "@/types"
import type { DataPoint } from "./area-chart"
import { $containerFilter } from "@/lib/stores"
/** Chart configurations for CPU, memory, and network usage charts */
export interface ContainerChartConfigs {
cpu: ChartConfig
memory: ChartConfig
network: ChartConfig
}
/**
* Generates chart configurations for container metrics visualization
* @param containerData - Array of container statistics data points
* @returns Chart configurations for CPU, memory, and network metrics
*/
export function useContainerChartConfigs(containerData: ChartData["containerData"]): ContainerChartConfigs {
return useMemo(() => {
const configs = {
cpu: {} as ChartConfig,
memory: {} as ChartConfig,
network: {} as ChartConfig,
}
// Aggregate usage metrics for each container
const totalUsage = {
cpu: new Map<string, number>(),
memory: new Map<string, number>(),
network: new Map<string, number>(),
}
// Process each data point to calculate totals
for (let i = 0; i < containerData.length; i++) {
const stats = containerData[i]
const containerNames = Object.keys(stats)
for (let j = 0; j < containerNames.length; j++) {
const containerName = containerNames[j]
// Skip metadata field
if (containerName === "created") {
continue
}
const containerStats = stats[containerName]
if (!containerStats) {
continue
}
// Accumulate metrics for CPU, memory, and network
const currentCpu = totalUsage.cpu.get(containerName) ?? 0
const currentMemory = totalUsage.memory.get(containerName) ?? 0
const currentNetwork = totalUsage.network.get(containerName) ?? 0
const sentBytes = containerStats.b?.[0] ?? (containerStats.ns ?? 0) * 1024 * 1024
const recvBytes = containerStats.b?.[1] ?? (containerStats.nr ?? 0) * 1024 * 1024
totalUsage.cpu.set(containerName, currentCpu + (containerStats.c ?? 0))
totalUsage.memory.set(containerName, currentMemory + (containerStats.m ?? 0))
totalUsage.network.set(containerName, currentNetwork + sentBytes + recvBytes)
}
}
// Generate chart configurations for each metric type
Object.entries(totalUsage).forEach(([chartType, usageMap]) => {
const sortedContainers = Array.from(usageMap.entries()).sort(([, a], [, b]) => b - a)
const chartConfig = {} as Record<string, { label: string; color: string }>
const count = sortedContainers.length
// Generate colors for each container
for (let i = 0; i < count; i++) {
const [containerName] = sortedContainers[i]
const hue = ((i * 360) / count) % 360
chartConfig[containerName] = {
label: containerName,
color: `hsl(${hue}, var(--chart-saturation), var(--chart-lightness))`,
}
}
configs[chartType as keyof typeof configs] = chartConfig
})
return configs
}, [containerData])
}
/** Sets the correct width of the y axis in recharts based on the longest label */
export function useYAxisWidth() {
const [yAxisWidth, setYAxisWidth] = useState(0)
let maxChars = 0
let timeout: ReturnType<typeof setTimeout>
function updateYAxisWidth(str: string) {
if (str.length > maxChars) {
maxChars = str.length
const div = document.createElement("div")
div.className = "text-xs tabular-nums tracking-tighter table sr-only"
div.innerHTML = str
clearTimeout(timeout)
timeout = setTimeout(() => {
document.body.appendChild(div)
const width = div.offsetWidth + 20
if (width > yAxisWidth) {
setYAxisWidth(width)
}
document.body.removeChild(div)
})
}
return str
}
return { yAxisWidth, updateYAxisWidth }
}
/** Subscribes to the container filter store and returns filtered DataPoints for container charts */
export function useContainerDataPoints(
chartConfig: ChartConfig,
// biome-ignore lint/suspicious/noExplicitAny: container data records have dynamic keys
dataFn: (key: string, data: Record<string, any>) => number | null
) {
const filter = useStore($containerFilter)
const { dataPoints, filteredKeys } = useMemo(() => {
const filterTerms = filter
? filter
.toLowerCase()
.split(" ")
.filter((term) => term.length > 0)
: []
const filtered = new Set<string>()
const points = Object.keys(chartConfig).map((key) => {
const isFiltered = filterTerms.length > 0 && !filterTerms.some((term) => key.toLowerCase().includes(term))
if (isFiltered) filtered.add(key)
return {
label: key,
// biome-ignore lint/suspicious/noExplicitAny: container data records have dynamic keys
dataKey: (data: Record<string, any>) => dataFn(key, data),
color: chartConfig[key].color ?? "",
opacity: isFiltered ? 0.05 : 0.4,
strokeOpacity: isFiltered ? 0.1 : 1,
activeDot: !isFiltered,
stackId: "a",
}
})
return {
// biome-ignore lint/suspicious/noExplicitAny: container data records have dynamic keys
dataPoints: points as DataPoint<Record<string, any>>[],
filteredKeys: filtered,
}
}, [chartConfig, filter])
return { filter, dataPoints, filteredKeys }
}
// Assures consistent colors for network interfaces
export function useNetworkInterfaces(interfaces: SystemStats["ni"]) {
const keys = Object.keys(interfaces ?? {})
const sortedKeys = keys.sort((a, b) => (interfaces?.[b]?.[3] ?? 0) - (interfaces?.[a]?.[3] ?? 0))
return {
length: sortedKeys.length,
data: (index = 3) => {
return sortedKeys.map((key) => ({
label: key,
dataKey: ({ stats }: SystemStatsRecord) => stats?.ni?.[key]?.[index],
color: `hsl(${220 + (((sortedKeys.indexOf(key) * 360) / sortedKeys.length) % 360)}, 70%, 50%)`,
opacity: 0.3,
}))
},
}
}