"use client" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" import { fetchMonitor } from "@/lib/nezha-api" import { cn, formatTime } from "@/lib/utils" import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api" import { useQuery } from "@tanstack/react-query" import * as React from "react" import { useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts" import NetworkChartLoading from "./NetworkChartLoading" import { Label } from "./ui/label" import { Switch } from "./ui/switch" interface ResultItem { created_at: number [key: string]: number } export function NetworkChart({ server_id, show }: { server_id: number; show: boolean }) { const { t } = useTranslation() const { data: monitorData } = useQuery({ queryKey: ["monitor", server_id], queryFn: () => fetchMonitor(server_id), enabled: show, refetchOnMount: true, refetchOnWindowFocus: true, refetchInterval: 10000, }) if (!monitorData) return if (monitorData?.success && !monitorData.data) { return ( <>

{t("monitor.noData")}

) } const transformedData = transformData(monitorData.data) const formattedData = formatData(monitorData.data) const chartDataKey = Object.keys(transformedData) const initChartConfig = { avg_delay: { label: t("monitor.avgDelay"), }, ...chartDataKey.reduce((acc, key) => { acc[key] = { label: key, } return acc }, {} as ChartConfig), } satisfies ChartConfig return ( ) } export const NetworkChartClient = React.memo(function NetworkChart({ chartDataKey, chartConfig, chartData, serverName, formattedData, }: { chartDataKey: string[] chartConfig: ChartConfig chartData: ServerMonitorChart serverName: string formattedData: ResultItem[] }) { const { t } = useTranslation() const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false // Change from string to string array for multi-selection const [activeCharts, setActiveCharts] = React.useState([]) const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled) // Function to clear all selected charts const clearAllSelections = useCallback(() => { setActiveCharts([]) }, []) // Updated to handle multiple selections const handleButtonClick = useCallback((chart: string) => { setActiveCharts((prev) => { // If chart is already selected, remove it if (prev.includes(chart)) { return prev.filter((c) => c !== chart) } // Otherwise, add it to selected charts return [...prev, chart] }) }, []) const getColorByIndex = useCallback( (chart: string) => { const index = chartDataKey.indexOf(chart) return `hsl(var(--chart-${(index % 10) + 1}))` }, [chartDataKey], ) const chartButtons = useMemo( () => chartDataKey.map((key) => ( )), [chartDataKey, activeCharts, chartData, handleButtonClick], ) const chartLines = useMemo(() => { // If we have active charts selected, render only those if (activeCharts.length > 0) { return activeCharts.map((chart) => ( )) } // Otherwise show all charts (default view) return chartDataKey.map((key) => ( )) }, [activeCharts, chartDataKey, getColorByIndex]) const processedData = useMemo(() => { if (!isPeakEnabled) { // Always use formattedData when multiple charts are selected or none selected return formattedData } // For peak cutting, always use the formatted data which contains all series const data = formattedData const windowSize = 11 // 增加窗口大小以获取更好的统计效果 const alpha = 0.3 // EWMA平滑因子 // 辅助函数:计算中位数 const getMedian = (arr: number[]) => { const sorted = [...arr].sort((a, b) => a - b) const mid = Math.floor(sorted.length / 2) return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 } // 辅助函数:异常值处理 const processValues = (values: number[]) => { if (values.length === 0) return null const median = getMedian(values) const deviations = values.map((v) => Math.abs(v - median)) const medianDeviation = getMedian(deviations) * 1.4826 // MAD估计器 // 使用中位数绝对偏差(MAD)进行异常值检测 const validValues = values.filter( (v) => Math.abs(v - median) <= 3 * medianDeviation && // 更严格的异常值判定 v <= median * 3, // 限制最大值不超过中位数的3倍 ) if (validValues.length === 0) return median // 如果没有有效值,返回中位数 // 计算EWMA let ewma = validValues[0] for (let i = 1; i < validValues.length; i++) { ewma = alpha * validValues[i] + (1 - alpha) * ewma } return ewma } // 初始化EWMA历史值 const ewmaHistory: { [key: string]: number } = {} return data.map((point, index) => { if (index < windowSize - 1) return point const window = data.slice(index - windowSize + 1, index + 1) const smoothed = { ...point } as ResultItem // Process all chart keys or just the selected ones const keysToProcess = activeCharts.length > 0 ? activeCharts : chartDataKey keysToProcess.forEach((key) => { const values = window.map((w) => w[key]).filter((v) => v !== undefined && v !== null) as number[] if (values.length > 0) { const processed = processValues(values) if (processed !== null) { // Apply EWMA smoothing if (ewmaHistory[key] === undefined) { ewmaHistory[key] = processed } else { ewmaHistory[key] = alpha * processed + (1 - alpha) * ewmaHistory[key] } smoothed[key] = ewmaHistory[key] } } }) return smoothed }) }, [isPeakEnabled, activeCharts, formattedData, chartDataKey]) return (
{serverName} {chartDataKey.length} {t("monitor.monitorCount")}
{chartButtons}
{activeCharts.length > 0 && ( )} { if (array.length < 6) { return index === 0 || index === array.length - 1 } // 计算数据的总时间跨度(毫秒) const timeSpan = array[array.length - 1].created_at - array[0].created_at const hours = timeSpan / (1000 * 60 * 60) // 根据时间跨度调整显示间隔 if (hours <= 12) { // 12小时内,每60分钟显示一个刻度 return index === 0 || index === array.length - 1 || new Date(item.created_at).getMinutes() % 60 === 0 } // 超过12小时,每2小时显示一个刻度 const date = new Date(item.created_at) return date.getMinutes() === 0 && date.getHours() % 2 === 0 }) .map((item) => item.created_at)} tickFormatter={(value) => { const date = new Date(value) const minutes = date.getMinutes() return minutes === 0 ? `${date.getHours()}:00` : `${date.getHours()}:${minutes}` }} /> `${value}ms`} /> { return formatTime(payload[0].payload.created_at) }} /> } /> } /> {chartLines}
) }) const transformData = (data: NezhaMonitor[]) => { const monitorData: ServerMonitorChart = {} data.forEach((item) => { const monitorName = item.monitor_name if (!monitorData[monitorName]) { monitorData[monitorName] = [] } for (let i = 0; i < item.created_at.length; i++) { monitorData[monitorName].push({ created_at: item.created_at[i], avg_delay: item.avg_delay[i], }) } }) return monitorData } const formatData = (rawData: NezhaMonitor[]) => { const result: { [time: number]: ResultItem } = {} const allTimes = new Set() rawData.forEach((item) => { item.created_at.forEach((time) => allTimes.add(time)) }) const allTimeArray = Array.from(allTimes).sort((a, b) => a - b) rawData.forEach((item) => { const { monitor_name, created_at, avg_delay } = item allTimeArray.forEach((time) => { if (!result[time]) { result[time] = { created_at: time } } const timeIndex = created_at.indexOf(time) // @ts-expect-error - avg_delay is an array result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null }) }) return Object.values(result).sort((a, b) => a.created_at - b.created_at) }