"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 (
<>
>
)
}
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)
}