import { memo, useCallback, useMemo, useState, useEffect, useRef } from "react"; import { AreaChart, Area, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, } from "recharts"; import { ChartContainer } from "@/components/ui/chart"; import { Card, CardHeader, CardTitle } from "@/components/ui/card"; import type { NodeData, NodeStats } from "@/types/node"; import { formatBytes } from "@/utils"; import fillMissingTimePoints, { type RecordFormat } from "@/utils/RecordHelper"; import { Flex } from "@radix-ui/themes"; import Loading from "@/components/loading"; import { useNodeData } from "@/contexts/NodeDataContext"; interface LoadChartsProps { node: NodeData; hours: number; data?: RecordFormat[]; liveData?: NodeStats; } const ChartTitle = (text: string, left: React.ReactNode) => { return ( {text} {left} ); }; const LoadCharts = memo( ({ node, hours, data = [], liveData: live_data }: LoadChartsProps) => { const { getLoadHistory } = useNodeData(); const [historicalData, setHistoricalData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const isRealtime = hours === 0; useEffect(() => { if (!isRealtime) { const fetchHistoricalData = async () => { setLoading(true); setError(null); try { const data = await getLoadHistory(node.uuid, hours); setHistoricalData(data?.records || []); } catch (err: any) { setError(err.message || "Failed to fetch historical data"); } finally { setLoading(false); } }; fetchHistoricalData(); } else { // For realtime, we expect data to be passed via props. // We can set loading to false if data is present. setLoading(false); } }, [node.uuid, hours, getLoadHistory, isRealtime]); const minute = 60; const hour = minute * 60; const chartData = isRealtime ? data : hours === 1 ? fillMissingTimePoints(historicalData ?? [], minute, hour, minute * 2) : (() => { const interval = hours > 120 ? hour : minute * 15; const maxGap = interval * 2; return fillMissingTimePoints( historicalData ?? [], interval, hour * hours, maxGap ); })(); const chartDataLengthRef = useRef(0); chartDataLengthRef.current = chartData.length; const timeFormatter = useCallback((value: any, index: number) => { if (chartDataLengthRef.current === 0) { return ""; } if (index === 0 || index === chartDataLengthRef.current - 1) { return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); } return ""; }, []); const lableFormatter = useCallback( (value: any) => { const date = new Date(value); if (hours === 0) { return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", }); } return date.toLocaleString([], { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); }, [hours] ); const cn = "flex flex-col w-full h-full gap-4 justify-between"; const chartMargin = { top: 10, right: 10, bottom: 10, left: 10, }; const colors = ["#F38181", "#FCE38A", "#EAFFD0", "#95E1D3"]; const primaryColor = colors[0]; const secondaryColor = colors[1]; const percentageFormatter = (value: number) => { return `${value.toFixed(2)}%`; }; const memoryChartData = useMemo(() => { return chartData.map((item) => ({ time: item.time, ram: ((item.ram ?? 0) / (node?.mem_total ?? 1)) * 100, ram_raw: item.ram, swap: ((item.swap ?? 0) / (node?.swap_total ?? 1)) * 100, swap_raw: item.swap, })); }, [chartData, node?.mem_total, node?.swap_total]); // 通用自定义 Tooltip 组件 const CustomTooltip = ({ active, payload, label, chartType }: any) => { if (!active || !payload || !payload.length) return null; return (

{lableFormatter(label)}

{payload.map((item: any, index: number) => { let value = item.value; let displayName = item.name || item.dataKey; // 根据图表类型和 dataKey 格式化值和显示名称 switch (chartType) { case "cpu": value = percentageFormatter(value); displayName = "CPU 使用率"; break; case "memory": if (item.dataKey === "ram") { const rawValue = item.payload?.ram_raw; if (rawValue !== undefined) { value = `${formatBytes(rawValue)} (${value.toFixed(0)}%)`; } else { value = percentageFormatter(value); } displayName = "内存"; } else if (item.dataKey === "swap") { const rawValue = item.payload?.swap_raw; if (rawValue !== undefined) { value = `${formatBytes(rawValue)} (${value.toFixed(0)}%)`; } else { value = percentageFormatter(value); } displayName = "交换"; } break; case "disk": value = formatBytes(value); displayName = "磁盘使用"; break; case "network": if (item.dataKey === "net_in") { value = `${formatBytes(value)}/s`; displayName = "下载"; } else if (item.dataKey === "net_out") { value = `${formatBytes(value)}/s`; displayName = "上传"; } break; case "connections": if (item.dataKey === "connections") { displayName = "TCP 连接"; } else if (item.dataKey === "connections_udp") { displayName = "UDP 连接"; } value = value.toString(); break; case "process": displayName = "进程数"; value = value.toString(); break; default: value = value.toString(); } return (
{displayName}:
{value}
); })}
); }; return (
{loading && (
)} {error && (

{error}

)}
{/* CPU */} {ChartTitle( "CPU", live_data?.cpu?.usage ? `${live_data.cpu.usage.toFixed(2)}%` : "-" )} index !== 0 ? `${value}%` : "" } orientation="left" type="number" tick={{ dx: -10 }} mirror={true} /> ( )} /> {/* Ram */} {ChartTitle( "内存", )} index !== 0 ? `${value}%` : "" } orientation="left" type="number" tick={{ dx: -10 }} mirror={true} /> ( )} /> {/* Disk */} {ChartTitle( "磁盘", live_data?.disk?.used ? `${formatBytes(live_data.disk.used)} / ${formatBytes( node?.disk_total || 0 )}` : "-" )} index !== 0 ? `${formatBytes(value)}` : "" } orientation="left" type="number" tick={{ dx: -10 }} mirror={true} /> ( )} /> {/* Network */} {ChartTitle( "网络", ↑ {formatBytes(live_data?.network.up || 0)}/s ↓ {formatBytes(live_data?.network.down || 0)}/s )} index !== 0 ? `${formatBytes(value)}` : "" } orientation="left" type="number" tick={{ dx: -10 }} mirror={true} /> ( )} /> {/* Connections */} {ChartTitle( "连接数", TCP: {live_data?.connections.tcp} UDP: {live_data?.connections.udp} )} index !== 0 ? `${value}` : "" } orientation="left" type="number" tick={{ dx: -10 }} mirror={true} /> ( )} /> {/* Process */} {ChartTitle("进程数", live_data?.process || "-")} index !== 0 ? `${value}` : "" } orientation="left" type="number" tick={{ dx: -10 }} mirror={true} /> ( )} />
); } ); export default LoadCharts;