"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { Box, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; import type { WidgetComponentProps } from "../definition"; import { CombinedNetworkTrafficChart } from "./chart/combined-network-traffic"; import { SystemResourceCPUChart } from "./chart/cpu-chart"; import { SystemResourceGPUChart } from "./chart/gpu-chart"; import { SystemResourceMemoryChart } from "./chart/memory-chart"; import { NetworkTrafficChart } from "./chart/network-traffic"; const COMPACT_HISTORY_SIZE = 15; const ADVANCED_HISTORY_SIZE = 60; type ChartItem = { cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null; }; export const toChartItem = (healthInfo: { cpuUtilization: number; memUsedInBytes: number; gpu: { processorUtilization: number }[]; network: { up: number; down: number } | null; }): ChartItem => ({ cpu: healthInfo.cpuUtilization, memory: healthInfo.memUsedInBytes, gpu: healthInfo.gpu.length > 0 ? healthInfo.gpu.reduce((acc, gpu) => acc + gpu.processorUtilization, 0) / healthInfo.gpu.length : 0, network: healthInfo.network, }); export const appendBoundedHistory = (history: ChartItem[], item: ChartItem, limit: number): ChartItem[] => [...history, item].slice(-limit); export default function SystemResources({ integrationIds, options, width, displayMode, }: WidgetComponentProps<"systemResources">) { const { data = [], dataUpdatedAt } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ integrationIds, }); const isAdvanced = displayMode === "advanced"; const integrationKey = useMemo(() => integrationIds.join("\u0000"), [integrationIds]); const [historyByIntegration, setHistoryByIntegration] = useState>({}); const previousIntegrationKey = useRef(integrationKey); useEffect(() => { if (previousIntegrationKey.current === integrationKey) return; previousIntegrationKey.current = integrationKey; setHistoryByIntegration({}); }, [integrationKey]); const lastUpdatedAt = useRef(dataUpdatedAt); useEffect(() => { if (dataUpdatedAt === lastUpdatedAt.current) return; lastUpdatedAt.current = dataUpdatedAt; setHistoryByIntegration((previous) => Object.fromEntries( data.map((entry) => [ entry.integrationId, appendBoundedHistory( previous[entry.integrationId] ?? [], toChartItem(entry.healthInfo), ADVANCED_HISTORY_SIZE, ), ]), ), ); }, [dataUpdatedAt, data]); const panelColumns = isAdvanced && width >= 960 && data.length > 1 ? 2 : 1; const panelWidth = width / panelColumns; const renderCharts = (entry: (typeof data)[number], availableWidth: number) => { const currentItem = toChartItem(entry.healthInfo); const fullHistory = historyByIntegration[entry.integrationId] ?? [currentItem]; const history = isAdvanced ? fullHistory : fullHistory.slice(-COMPACT_HISTORY_SIZE); return ( 1} /> ); }; if (!isAdvanced && data.length === 1 && data[0]) { return ( {renderCharts(data[0], width)} ); } return ( {data.map((entry) => renderCharts(entry, panelWidth))} ); } interface SystemChartsProps { integrationName: string; items: ChartItem[]; memoryCapacityInBytes: number; options: WidgetComponentProps<"systemResources">["options"]; width: number; isAdvanced: boolean; showTitle: boolean; } const SystemCharts = ({ integrationName, items, memoryCapacityInBytes, options, width, isAdvanced, showTitle, }: SystemChartsProps) => { const showNetwork = items.length > 0 && items.every((item) => item.network !== null) && options.visibleCharts.includes("network"); const chartCount = options.visibleCharts.filter((chart) => chart !== "network").length + Number(showNetwork); const chartColumns = isAdvanced && width >= 560 ? 2 : 1; const chartHeight = isAdvanced ? 180 : `calc((100% - ${Math.max(0, chartCount - 1) * 8}px) / ${Math.max(1, chartCount)})`; return ( {showTitle && ( {integrationName} )} {options.visibleCharts.includes("cpu") && ( item.cpu)} hasShadow={options.hasShadow} labelDisplayMode={options.labelDisplayMode} /> )} {options.visibleCharts.includes("memory") && ( item.memory)} totalCapacityInBytes={memoryCapacityInBytes} hasShadow={options.hasShadow} labelDisplayMode={options.labelDisplayMode} /> )} {options.visibleCharts.includes("gpu") && ( item.gpu)} hasShadow={options.hasShadow} labelDisplayMode={options.labelDisplayMode} /> )} {showNetwork && (width >= 300 ? ( item.network?.down ?? 0)} isUp={false} hasShadow={options.hasShadow} labelDisplayMode={options.labelDisplayMode} /> item.network?.up ?? 0)} isUp hasShadow={options.hasShadow} labelDisplayMode={options.labelDisplayMode} /> ) : ( item.network ?? { up: 0, down: 0 })} hasShadow={options.hasShadow} labelDisplayMode={options.labelDisplayMode} /> ))} ); };