import { $systems, pb, $chartTime, $containerFilter, $userSettings } from '@/lib/stores' import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types' 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, LayoutGridIcon, StretchHorizontalIcon, XIcon, } from 'lucide-react' import ChartTimeSelect from '../charts/chart-time-select' import { chartTimeData, cn, getPbTimestamp, useClampedIsInViewport, useLocalStorage, } from '@/lib/utils' import { Separator } from '../ui/separator' import { scaleTime } from 'd3-scale' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' import { Button, buttonVariants } from '../ui/button' import { Input } from '../ui/input' const CpuChart = lazy(() => import('../charts/cpu-chart')) const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart')) const MemChart = lazy(() => import('../charts/mem-chart')) const ContainerMemChart = lazy(() => import('../charts/container-mem-chart')) const DiskChart = lazy(() => import('../charts/disk-chart')) const DiskIoChart = lazy(() => import('../charts/disk-io-chart')) const BandwidthChart = lazy(() => import('../charts/bandwidth-chart')) const ContainerNetChart = lazy(() => import('../charts/container-net-chart')) const SwapChart = lazy(() => import('../charts/swap-chart')) const TemperatureChart = lazy(() => import('../charts/temperature-chart')) const ExFsDiskChart = lazy(() => import('../charts/extra-fs-disk-chart')) const ExFsDiskIoChart = lazy(() => import('../charts/extra-fs-disk-io-chart')) export default function SystemDetail({ name }: { name: string }) { const systems = useStore($systems) const chartTime = useStore($chartTime) const [grid, setGrid] = useLocalStorage('grid', true) const [ticks, setTicks] = useState([] as number[]) const [system, setSystem] = useState({} as SystemRecord) const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[]) const [hasDockerStats, setHasDocker] = useState(false) const netCardRef = useRef(null) const [dockerCpuChartData, setDockerCpuChartData] = useState[]>( [] ) const [dockerMemChartData, setDockerMemChartData] = useState[]>( [] ) const [dockerNetChartData, setDockerNetChartData] = useState[]>( [] ) useEffect(() => { document.title = `${name} / Beszel` return () => { resetCharts() $chartTime.set($userSettings.get().chartTime) $containerFilter.set('') setHasDocker(false) } }, [name]) function resetCharts() { setSystemStats([]) setDockerCpuChartData([]) setDockerMemChartData([]) setDockerNetChartData([]) } useEffect(resetCharts, [chartTime]) useEffect(() => { if (system.id && system.name === name) { return } const matchingSystem = systems.find((s) => s.name === name) as SystemRecord if (matchingSystem) { setSystem(matchingSystem) } }, [name, system, systems]) // update system when new data is available useEffect(() => { if (!system.id) { return } pb.collection('systems').subscribe(system.id, (e) => { setSystem(e.record) }) return () => { pb.collection('systems').unsubscribe(system.id) } }, [system]) async function getStats(collection: string): Promise { return await pb.collection(collection).getFullList({ filter: pb.filter('system={:id} && created > {:created} && type={:type}', { id: system.id, created: getPbTimestamp(chartTime), 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 } // get stats useEffect(() => { if (!system.id || !chartTime) { return } Promise.allSettled([ getStats('system_stats'), getStats('container_stats'), ]).then(([systemStats, containerStats]) => { const expectedInterval = chartTimeData[chartTime].expectedInterval if (containerStats.status === 'fulfilled' && containerStats.value.length) { makeContainerData(addEmptyValues(containerStats.value, expectedInterval)) setHasDocker(true) } if (systemStats.status === 'fulfilled') { setSystemStats(addEmptyValues(systemStats.value, expectedInterval)) } }) }, [system, chartTime]) useEffect(() => { if (!systemStats.length) { return } const now = new Date() const startTime = chartTimeData[chartTime].getOffset(now) const scale = scaleTime([startTime.getTime(), now], [0, systemStats.length]) setTicks(scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime())) }, [chartTime, systemStats]) // make container stats for charts const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => { // console.log('containers', containers) const dockerCpuData = [] const dockerMemData = [] const dockerNetData = [] for (let { created, stats } of containers) { if (!created) { let nullData = { time: null } as unknown dockerCpuData.push(nullData as Record) dockerMemData.push(nullData as Record) dockerNetData.push(nullData as Record) continue } const time = new Date(created).getTime() 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 netData[container.n] = [container.ns, container.nr, container.ns + container.nr] // sent, received, total } dockerCpuData.push(cpuData) dockerMemData.push(memData) dockerNetData.push(netData) } setDockerCpuChartData(dockerCpuData) setDockerMemChartData(dockerMemData) setDockerNetChartData(dockerNetData) }, []) const uptime = useMemo(() => { let uptime = system.info?.u || 0 if (uptime < 172800) { return `${Math.trunc(uptime / 3600)} hours` } return `${Math.trunc(system.info?.u / 86400)} days` }, [system.info?.u]) /** Space for tooltip if more than 12 containers */ const bottomSpacing = useMemo(() => { if (!netCardRef.current || !dockerNetChartData.length) { return 0 } const tooltipHeight = (Object.keys(dockerNetChartData[0]).length - 11) * 17.8 - 40 const wrapperEl = document.getElementById('chartwrap') as HTMLDivElement const wrapperRect = wrapperEl.getBoundingClientRect() const chartRect = netCardRef.current.getBoundingClientRect() const distanceToBottom = wrapperRect.bottom - chartRect.bottom return tooltipHeight - distanceToBottom }, [netCardRef.current, dockerNetChartData]) if (!system.id) { return null } return ( <>
{/* system info */}

{system.name}

{system.status === 'up' && ( )} {system.status}
{system.host}
{system.info?.u && (
{uptime}
Uptime
)} {system.info?.m && ( <>
{system.info.m} ({system.info.c}c / {system.info.t}t)
)}
Toggle grid
{/* main charts */}
{hasDockerStats && ( )} {hasDockerStats && ( )} {(systemStats.at(-1)?.stats.s ?? 0) > 0 && ( )} {hasDockerStats && dockerNetChartData.length > 0 && (
)} {systemStats.at(-1)?.stats.t && ( )}
{/* extra filesystem charts */} {Object.keys(systemStats.at(-1)?.stats.efs ?? {}).length > 0 && (
{Object.keys(systemStats.at(-1)?.stats.efs ?? {}).map((extraFsName) => { return (
) })}
)}
{/* add space for tooltip if more than 12 containers */} {bottomSpacing > 0 && } ) } function ContainerFilterBar() { const containerFilter = useStore($containerFilter) const handleChange = useCallback((e: React.ChangeEvent) => { $containerFilter.set(e.target.value) }, []) // Use an empty dependency array to prevent re-creation return (
{containerFilter && ( )}
) } function ChartCard({ title, description, children, grid, isContainerChart, }: { title: string description: string children: React.ReactNode grid?: boolean isContainerChart?: boolean }) { const target = useRef(null) const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target }) return ( {title} {description} {isContainerChart && } {} {isInViewport && {children}} ) }