diff --git a/src/components/NetworkChart.tsx b/src/components/NetworkChart.tsx index 01b7b10..7eeee0f 100644 --- a/src/components/NetworkChart.tsx +++ b/src/components/NetworkChart.tsx @@ -1,6 +1,7 @@ "use client"; import { useQuery } from "@tanstack/react-query"; +import { m } from "framer-motion"; import * as React from "react"; import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; @@ -44,12 +45,6 @@ interface ResultItem { [key: string]: number; } -const TIME_RANGE_OPTIONS: { value: MonitorPeriod; label: string }[] = [ - { value: "1d", label: "1D" }, - { value: "7d", label: "7D" }, - { value: "30d", label: "30D" }, -]; - /** * Helper method to calculate packet loss from delay data */ @@ -237,6 +232,12 @@ export const NetworkChartClient = React.memo(function NetworkChart({ }) { const { t } = useTranslation(); + const TIME_RANGE_OPTIONS: { value: MonitorPeriod; label: string }[] = [ + { value: "1d", label: t("monitor.period1d") }, + { value: "7d", label: t("monitor.period7d") }, + { value: "30d", label: t("monitor.period30d") }, + ]; + const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage @@ -529,183 +530,201 @@ export const NetworkChartClient = React.memo(function NetworkChart({ }, [isPeakEnabled, activeCharts, formattedData, chartData, chartDataKey]); return ( - - -
- - {serverName} - - - {chartDataKey.length} {t("monitor.monitorCount")} - -
-
- {TIME_RANGE_OPTIONS.map((option) => { - const isLocked = !isLogin && option.value !== "1d"; - return ( - - ); - })} -
-
- - -
-
-
-
{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}`; +
+
+
+ {TIME_RANGE_OPTIONS.map((option) => { + const isLocked = !isLogin && option.value !== "1d"; + return ( +
{ + if (!isLocked) { + onPeriodChange(option.value); + } }} - /> - `${value}ms`} - /> - {activeCharts.length === 1 && ( + className={cn( + "relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300", + period === option.value + ? "text-foreground" + : "text-muted-foreground hover:text-foreground", + isLocked && "cursor-not-allowed opacity-40 grayscale", + )} + > + {period === option.value && ( + + )} + {option.label} +
+ ); + })} +
+
+ + +
+
+ + +
+ + {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}%`} + tickFormatter={(value) => `${value}ms`} /> - )} - { - return formatTime(payload[0].payload.created_at); - }} - formatter={(value, name) => { - let formattedValue: string; - let label: string; - - if (name === "packet_loss") { - formattedValue = `${Number(value).toFixed(2)}%`; - label = t("monitor.packetLoss", "Packet Loss"); - } else if (name === "avg_delay") { - formattedValue = `${Number(value).toFixed(2)}ms`; - label = t("monitor.avgDelay", "Avg Delay"); - } else { - // For monitor names (in multi-chart view) - delay data - formattedValue = `${Number(value).toFixed(2)}ms`; - label = name as string; - } - - return ( -
- {label} - - {formattedValue} - -
- ); - }} + {activeCharts.length === 1 && ( + `${value}%`} /> - } - /> - {activeCharts.length !== 1 && ( - } /> - )} - {chartElements} -
-
-
-
-
+ )} + { + return formatTime(payload[0].payload.created_at); + }} + formatter={(value, name) => { + let formattedValue: string; + let label: string; + + if (name === "packet_loss") { + formattedValue = `${Number(value).toFixed(2)}%`; + label = t("monitor.packetLoss", "Packet Loss"); + } else if (name === "avg_delay") { + formattedValue = `${Number(value).toFixed(2)}ms`; + label = t("monitor.avgDelay", "Avg Delay"); + } else { + // For monitor names (in multi-chart view) - delay data + formattedValue = `${Number(value).toFixed(2)}ms`; + label = name as string; + } + + return ( +
+ + {label} + + + {formattedValue} + +
+ ); + }} + /> + } + /> + {activeCharts.length !== 1 && ( + } /> + )} + {chartElements} + + +
+ + +
); }); diff --git a/src/components/ServerDetailChart.tsx b/src/components/ServerDetailChart.tsx index be7631b..ad42606 100644 --- a/src/components/ServerDetailChart.tsx +++ b/src/components/ServerDetailChart.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { m } from "framer-motion"; import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { @@ -32,6 +33,7 @@ import type { NezhaWebsocketResponse, } from "@/types/nezha-api"; +import ChartSkeleton from "./loading/ChartSkeleton"; import { ServerDetailChartLoading } from "./loading/ServerDetailLoading"; import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"; @@ -94,31 +96,41 @@ function PeriodSelector({ ]; return ( -
+
{periods.map((period) => { // Only realtime and 1d are available for non-logged-in users const isLocked = !isLogin && period.value !== "realtime" && period.value !== "1d"; return ( - + {selectedPeriod === period.value && ( + + )} +
+ {period.value === "realtime" && ( + + )} + {period.label} +
+
); })}
@@ -261,15 +273,24 @@ function useHistoricalData( ) { const [historicalData, setHistoricalData] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [displayData, setDisplayData] = useState([]); + const loadingTimerRef = useRef(null); useEffect(() => { if (period === "realtime") { setHistoricalData([]); + setDisplayData([]); + if (loadingTimerRef.current) { + clearTimeout(loadingTimerRef.current); + } return; } const fetchData = async () => { - setIsLoading(true); + loadingTimerRef.current = setTimeout(() => { + setIsLoading(true); + }, 200); + try { const response = await fetchServerMetrics( serverId, @@ -281,10 +302,15 @@ function useHistoricalData( transformData(point.ts, point.value), ); setHistoricalData(transformedData); + setDisplayData(transformedData); } } catch (error) { console.error(`Failed to fetch ${metricName} metrics:`, error); } finally { + if (loadingTimerRef.current) { + clearTimeout(loadingTimerRef.current); + loadingTimerRef.current = null; + } setIsLoading(false); } }; @@ -292,7 +318,7 @@ function useHistoricalData( fetchData(); }, [serverId, metricName, period, transformData]); - return { historicalData, isLoading }; + return { historicalData, displayData, isLoading }; } function GpuChart({ @@ -328,12 +354,8 @@ function GpuChart({ [], ); - const { historicalData, isLoading } = useHistoricalData( - id, - "gpu", - period, - transformGpuData, - ); + const { displayData: gpuHistoricalData, isLoading } = + useHistoricalData(id, "gpu", period, transformGpuData); // 初始化历史数据 useEffect(() => { @@ -397,7 +419,7 @@ function GpuChart({ }, } satisfies ChartConfig; - const displayData = period === "realtime" ? gpuChartData : historicalData; + const displayData = period === "realtime" ? gpuChartData : gpuHistoricalData; return ( {isLoading ? ( -
- - Loading... - -
+ ) : ( ( - data.id, - "cpu", - period, - transformCpuData, - ); + const { displayData: cpuHistoricalData, isLoading } = + useHistoricalData(data.id, "cpu", period, transformCpuData); // 初始化历史数据 useEffect(() => { @@ -602,7 +616,7 @@ function CpuChart({ }, } satisfies ChartConfig; - const displayData = period === "realtime" ? cpuChartData : historicalData; + const displayData = period === "realtime" ? cpuChartData : cpuHistoricalData; return ( {isLoading ? ( -
- - Loading... - -
+ ) : ( ( - data.id, - "process_count", - period, - transformProcessData, - ); + const { displayData: processHistoricalData, isLoading } = + useHistoricalData( + data.id, + "process_count", + period, + transformProcessData, + ); // 初始化历史数据 useEffect(() => { @@ -807,7 +818,8 @@ function ProcessChart({ }, } satisfies ChartConfig; - const displayData = period === "realtime" ? processChartData : historicalData; + const displayData = + period === "realtime" ? processChartData : processHistoricalData; return ( {isLoading ? ( -
- - Loading... - -
+ ) : ( {isLoadingMem ? ( -
- - Loading... - -
+ ) : ( ( - data.id, - "disk", - period, - transformDiskData, - ); + const { displayData: diskHistoricalData, isLoading } = + useHistoricalData( + data.id, + "disk", + period, + transformDiskData, + ); // 初始化历史数据 useEffect(() => { @@ -1308,7 +1313,8 @@ function DiskChart({ }, } satisfies ChartConfig; - const displayData = period === "realtime" ? diskChartData : historicalData; + const displayData = + period === "realtime" ? diskChartData : diskHistoricalData; return ( {isLoading ? ( -
- - Loading... - -
+ ) : ( {isLoadingNetwork ? ( -
- - Loading... - -
+ ) : ( {isLoadingConnect ? ( -
- - Loading... - -
+ ) : ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index f4f9ed5..5e961b0 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -47,7 +47,11 @@ "noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu", "avgDelay": "Latenz", "packetLoss": "Paketverlust", - "clearSelections": "Löschen" + "clearSelections": "Löschen", + "peakCut": "Peak cut", + "period1d": "1 Tag", + "period7d": "7 Tage", + "period30d": "30 Tage" }, "billingInfo": { "error": "Fehler", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 2ec5fb7..441abe7 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -113,7 +113,11 @@ "avgDelay": "Latency", "monitorCount": "Services", "packetLoss": "Packet Loss", - "clearSelections": "Clear" + "clearSelections": "Clear", + "peakCut": "Peak cut", + "period1d": "1 Day", + "period7d": "7 Days", + "period30d": "30 Days" }, "pwa": { "offlineReady": "App ready to work offline", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index d7458bf..8591be7 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -93,7 +93,11 @@ "noData": "No hay datos de servidores, primero agregue un monitor de servicio", "monitorCount": "Servicios", "packetLoss": "Pérdida de paquetes", - "clearSelections": "Limpiar" + "clearSelections": "Limpiar", + "peakCut": "Peak cut", + "period1d": "1 Día", + "period7d": "7 Días", + "period30d": "30 Días" }, "error": { "pageNotFound": "Página no encontrada", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 5f71492..c396b25 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -100,7 +100,11 @@ "avgDelay": "Задержка", "monitorCount": "Сервисы", "packetLoss": "Потеря пакетов", - "clearSelections": "Очистить" + "clearSelections": "Очистить", + "peakCut": "Peak cut", + "period1d": "1 день", + "period7d": "7 дней", + "period30d": "30 дней" }, "pwa": { "newContent": "Доступен новый контент", diff --git a/src/locales/ta/translation.json b/src/locales/ta/translation.json index d6391dd..95659d7 100644 --- a/src/locales/ta/translation.json +++ b/src/locales/ta/translation.json @@ -95,7 +95,11 @@ "avgDelay": "சுணக்கம்", "monitorCount": "சேவைகள்", "packetLoss": "தொகுப்பு இழப்பு", - "clearSelections": "அழி" + "clearSelections": "அழி", + "peakCut": "Peak cut", + "period1d": "1 நாள்", + "period7d": "7 நாட்கள்", + "period30d": "30 நாட்கள்" }, "pwa": { "offlineReady": "ஆஃப்லைனில் வேலை செய்ய பயன்பாடு தயாராக உள்ளது", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index d8f9690..307c915 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -114,7 +114,11 @@ "avgDelay": "延迟", "monitorCount": "个监控服务", "packetLoss": "丢包率", - "clearSelections": "清除" + "clearSelections": "清除", + "peakCut": "削峰", + "period1d": "1 天", + "period7d": "7 天", + "period30d": "30 天" }, "pwa": { "offlineReady": "应用可以离线使用了", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index c6ca133..2a56a48 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -112,7 +112,11 @@ "avgDelay": "延遲", "monitorCount": "個監控", "packetLoss": "丟包率", - "clearSelections": "清除" + "clearSelections": "清除", + "peakCut": "削峰", + "period1d": "1 天", + "period7d": "7 天", + "period30d": "30 天" }, "billingInfo": { "remaining": "剩餘天數",