diff --git a/src/components/ServerDetailChart.tsx b/src/components/ServerDetailChart.tsx index 86e6542..fd9ad4e 100644 --- a/src/components/ServerDetailChart.tsx +++ b/src/components/ServerDetailChart.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Area, @@ -13,12 +14,19 @@ import { Card, CardContent } from "@/components/ui/card"; import { type ChartConfig, ChartContainer } from "@/components/ui/chart"; import { useWebSocketContext } from "@/hooks/use-websocket-context"; import { formatBytes } from "@/lib/format"; +import { fetchLoginUser, fetchServerMetrics } from "@/lib/nezha-api"; import { cn, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"; -import type { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api"; +import type { + MetricPeriod, + NezhaServer, + NezhaWebsocketResponse, +} from "@/types/nezha-api"; import { ServerDetailChartLoading } from "./loading/ServerDetailLoading"; import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"; +type ChartPeriod = "realtime" | MetricPeriod; + type gpuChartData = { timeStamp: string; gpu: number; @@ -57,12 +65,86 @@ type connectChartData = { udp: number; }; +function PeriodSelector({ + selectedPeriod, + onPeriodChange, + isLogin, +}: { + selectedPeriod: ChartPeriod; + onPeriodChange: (period: ChartPeriod) => void; + isLogin: boolean; +}) { + const { t } = useTranslation(); + + const periods: { value: ChartPeriod; label: string }[] = [ + { value: "realtime", label: t("serverDetailChart.realtime") }, + { value: "1d", label: t("serverDetailChart.period1d") }, + { value: "7d", label: t("serverDetailChart.period7d") }, + { value: "30d", label: t("serverDetailChart.period30d") }, + ]; + + 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 ( + + ); + })} +
+ ); +} + export default function ServerDetailChart({ server_id, }: { server_id: string; }) { const { lastMessage, connected, messageHistory } = useWebSocketContext(); + const [selectedPeriod, setSelectedPeriod] = useState("realtime"); + + // Check if user is logged in + const { data: userData, isError: isLoginError } = useQuery({ + queryKey: ["login-user"], + queryFn: () => fetchLoginUser(), + refetchOnMount: false, + refetchOnWindowFocus: true, + refetchIntervalInBackground: true, + refetchInterval: 1000 * 30, + retry: 0, + }); + const isLogin = isLoginError + ? false + : userData + ? !!userData?.data?.id && !!document.cookie + : false; + + // Reset period if user is not logged in and selected period is restricted + useEffect(() => { + if (!isLogin && selectedPeriod !== "realtime" && selectedPeriod !== "1d") { + setSelectedPeriod("1d"); + } + }, [isLogin, selectedPeriod]); if (!connected && !lastMessage) { return ; @@ -86,72 +168,130 @@ export default function ServerDetailChart({ const gpuList = server.host.gpu || []; return ( -
- + - {gpuStats.length >= 1 && gpuList.length === gpuStats.length - ? gpuList.map((gpu, index) => ( - - )) - : gpuStats.length > 0 - ? gpuStats.map((gpu, index) => ( +
+ + {gpuStats.length >= 1 && gpuList.length === gpuStats.length + ? gpuList.map((gpu, index) => ( )) - : null} - - - - - + : gpuStats.length > 0 + ? gpuStats.map((gpu, index) => ( + + )) + : null} + + + + + +
); } +function useHistoricalData( + serverId: number, + metricName: string, + period: ChartPeriod, + transformData: (timestamp: number, value: number) => T, +) { + const [historicalData, setHistoricalData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (period === "realtime") { + setHistoricalData([]); + return; + } + + const fetchData = async () => { + setIsLoading(true); + try { + const response = await fetchServerMetrics( + serverId, + metricName as Parameters[1], + period as MetricPeriod, + ); + if (response.success && response.data?.data_points) { + const transformedData = response.data.data_points.map((point) => + transformData(point.ts, point.value), + ); + setHistoricalData(transformedData); + } + } catch (error) { + console.error(`Failed to fetch ${metricName} metrics:`, error); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, [serverId, metricName, period, transformData]); + + return { historicalData, isLoading }; +} + function GpuChart({ id, index, gpuStat, gpuName, messageHistory, + period, }: { now: number; id: number; @@ -159,6 +299,7 @@ function GpuChart({ gpuStat: number; gpuName?: string; messageHistory: { data: string }[]; + period: ChartPeriod; }) { const [gpuChartData, setGpuChartData] = useState([]); const hasInitialized = useRef(false); @@ -169,9 +310,28 @@ function GpuChart({ ? window.CustomBackgroundImage : undefined; + const transformGpuData = useMemo( + () => (timestamp: number, value: number) => ({ + timeStamp: timestamp.toString(), + gpu: value, + }), + [], + ); + + const { historicalData, isLoading } = useHistoricalData( + id, + "gpu", + period, + transformGpuData, + ); + // 初始化历史数据 useEffect(() => { - if (!hasInitialized.current && messageHistory.length > 0) { + if ( + period === "realtime" && + !hasInitialized.current && + messageHistory.length > 0 + ) { const historyData = messageHistory .map((msg) => { const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse; @@ -190,10 +350,18 @@ function GpuChart({ hasInitialized.current = true; setHistoryLoaded(true); } - }, [messageHistory, id, index]); + }, [messageHistory, id, index, period]); + + // Reset when switching to realtime + useEffect(() => { + if (period === "realtime") { + hasInitialized.current = false; + setHistoryLoaded(false); + } + }, [period]); useEffect(() => { - if (gpuStat && historyLoaded) { + if (gpuStat && historyLoaded && period === "realtime") { const timestamp = Date.now().toString(); setGpuChartData((prevData) => { let newData = [] as gpuChartData[]; @@ -211,7 +379,7 @@ function GpuChart({ return newData; }); } - }, [gpuStat, historyLoaded]); + }, [gpuStat, historyLoaded, period]); const chartConfig = { gpu: { @@ -219,6 +387,8 @@ function GpuChart({ }, } satisfies ChartConfig; + const displayData = period === "realtime" ? gpuChartData : historicalData; + return ( - - - formatRelativeTime(value)} - /> - `${value}%`} - /> - - + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + + + formatRelativeTime(value)} + /> + `${value}%`} + /> + + + )} @@ -296,10 +474,12 @@ function CpuChart({ now, data, messageHistory, + period, }: { now: number; data: NezhaServer; messageHistory: { data: string }[]; + period: ChartPeriod; }) { const [cpuChartData, setCpuChartData] = useState([]); const hasInitialized = useRef(false); @@ -312,9 +492,28 @@ function CpuChart({ ? window.CustomBackgroundImage : undefined; + const transformCpuData = useMemo( + () => (timestamp: number, value: number) => ({ + timeStamp: timestamp.toString(), + cpu: value, + }), + [], + ); + + const { historicalData, isLoading } = useHistoricalData( + data.id, + "cpu", + period, + transformCpuData, + ); + // 初始化历史数据 useEffect(() => { - if (!hasInitialized.current && messageHistory.length > 0) { + if ( + period === "realtime" && + !hasInitialized.current && + messageHistory.length > 0 + ) { const historyData = messageHistory .map((msg) => { const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse; @@ -327,17 +526,25 @@ function CpuChart({ }; }) .filter((item): item is cpuChartData => item !== null) - .reverse(); // 保持时间顺序 + .reverse(); setCpuChartData(historyData); hasInitialized.current = true; setHistoryLoaded(true); } - }, [messageHistory, data.id]); + }, [messageHistory, data.id, period]); + + // Reset when switching to realtime + useEffect(() => { + if (period === "realtime") { + hasInitialized.current = false; + setHistoryLoaded(false); + } + }, [period]); // 更新实时数据 useEffect(() => { - if (data && historyLoaded) { + if (data && historyLoaded && period === "realtime") { const timestamp = Date.now().toString(); setCpuChartData((prevData) => { let newData = [] as cpuChartData[]; @@ -355,7 +562,7 @@ function CpuChart({ return newData; }); } - }, [data, historyLoaded, cpu]); + }, [data, historyLoaded, cpu, period]); const chartConfig = { cpu: { @@ -363,6 +570,8 @@ function CpuChart({ }, } satisfies ChartConfig; + const displayData = period === "realtime" ? cpuChartData : historicalData; + return ( - - - formatRelativeTime(value)} - /> - `${value}%`} - /> - - + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + + + formatRelativeTime(value)} + /> + `${value}%`} + /> + + + )} @@ -437,10 +654,12 @@ function ProcessChart({ now, data, messageHistory, + period, }: { now: number; data: NezhaServer; messageHistory: { data: string }[]; + period: ChartPeriod; }) { const { t } = useTranslation(); const [processChartData, setProcessChartData] = useState( @@ -456,9 +675,28 @@ function ProcessChart({ const { process } = formatNezhaInfo(now, data); + const transformProcessData = useMemo( + () => (timestamp: number, value: number) => ({ + timeStamp: timestamp.toString(), + process: value, + }), + [], + ); + + const { historicalData, isLoading } = useHistoricalData( + data.id, + "process_count", + period, + transformProcessData, + ); + // 初始化历史数据 useEffect(() => { - if (!hasInitialized.current && messageHistory.length > 0) { + if ( + period === "realtime" && + !hasInitialized.current && + messageHistory.length > 0 + ) { const historyData = messageHistory .map((msg) => { const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse; @@ -477,11 +715,19 @@ function ProcessChart({ hasInitialized.current = true; setHistoryLoaded(true); } - }, [messageHistory, data.id]); + }, [messageHistory, data.id, period]); + + // Reset when switching to realtime + useEffect(() => { + if (period === "realtime") { + hasInitialized.current = false; + setHistoryLoaded(false); + } + }, [period]); // 修改实时数据更新逻辑 useEffect(() => { - if (data && historyLoaded) { + if (data && historyLoaded && period === "realtime") { const timestamp = Date.now().toString(); setProcessChartData((prevData) => { let newData = [] as processChartData[]; @@ -499,7 +745,7 @@ function ProcessChart({ return newData; }); } - }, [data, historyLoaded, process]); + }, [data, historyLoaded, process, period]); const chartConfig = { process: { @@ -507,6 +753,8 @@ function ProcessChart({ }, } satisfies ChartConfig; + const displayData = period === "realtime" ? processChartData : historicalData; + return ( - - - formatRelativeTime(value)} - /> - - - + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + + + formatRelativeTime(value)} + /> + + + + )} @@ -572,10 +828,12 @@ function MemChart({ now, data, messageHistory, + period, }: { now: number; data: NezhaServer; messageHistory: { data: string }[]; + period: ChartPeriod; }) { const { t } = useTranslation(); const [memChartData, setMemChartData] = useState([] as memChartData[]); @@ -589,9 +847,70 @@ function MemChart({ const { mem, swap } = formatNezhaInfo(now, data); + // For memory, we fetch memory and swap separately and combine them + const [memHistoricalData, setMemHistoricalData] = useState( + [], + ); + const [isLoadingMem, setIsLoadingMem] = useState(false); + + useEffect(() => { + if (period === "realtime") { + setMemHistoricalData([]); + return; + } + + const fetchMemData = async () => { + setIsLoadingMem(true); + try { + const [memResponse, swapResponse] = await Promise.all([ + fetchServerMetrics(data.id, "memory", period as MetricPeriod), + fetchServerMetrics(data.id, "swap", period as MetricPeriod), + ]); + + if (memResponse.success && memResponse.data?.data_points) { + const swapMap = new Map(); + if (swapResponse.success && swapResponse.data?.data_points) { + for (const point of swapResponse.data.data_points) { + // Convert bytes to percentage + const swapPercent = + data.host.swap_total > 0 + ? (point.value / data.host.swap_total) * 100 + : 0; + swapMap.set(point.ts, swapPercent); + } + } + + const combinedData = memResponse.data.data_points.map((point) => { + // Convert bytes to percentage + const memPercent = + data.host.mem_total > 0 + ? (point.value / data.host.mem_total) * 100 + : 0; + return { + timeStamp: point.ts.toString(), + mem: memPercent, + swap: swapMap.get(point.ts) || 0, + }; + }); + setMemHistoricalData(combinedData); + } + } catch (error) { + console.error("Failed to fetch memory metrics:", error); + } finally { + setIsLoadingMem(false); + } + }; + + fetchMemData(); + }, [data.id, period, data.host.mem_total, data.host.swap_total]); + // 初始化历史数据 useEffect(() => { - if (!hasInitialized.current && messageHistory.length > 0) { + if ( + period === "realtime" && + !hasInitialized.current && + messageHistory.length > 0 + ) { const historyData = messageHistory .map((msg) => { const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse; @@ -611,11 +930,19 @@ function MemChart({ hasInitialized.current = true; setHistoryLoaded(true); } - }, [messageHistory, data.id]); + }, [messageHistory, data.id, period]); + + // Reset when switching to realtime + useEffect(() => { + if (period === "realtime") { + hasInitialized.current = false; + setHistoryLoaded(false); + } + }, [period]); // 修改实时数据更新逻辑 useEffect(() => { - if (data && historyLoaded) { + if (data && historyLoaded && period === "realtime") { const timestamp = Date.now().toString(); setMemChartData((prevData) => { let newData = [] as memChartData[]; @@ -633,7 +960,7 @@ function MemChart({ return newData; }); } - }, [data, historyLoaded, mem, swap]); + }, [data, historyLoaded, mem, swap, period]); const chartConfig = { mem: { @@ -644,6 +971,8 @@ function MemChart({ }, } satisfies ChartConfig; + const displayData = period === "realtime" ? memChartData : memHistoricalData; + return ( - - - formatRelativeTime(value)} - /> - `${value}%`} - /> - - - + {isLoadingMem ? ( +
+ + Loading... + +
+ ) : ( + + + formatRelativeTime(value)} + /> + `${value}%`} + /> + + + + )} @@ -761,10 +1098,12 @@ function DiskChart({ now, data, messageHistory, + period, }: { now: number; data: NezhaServer; messageHistory: { data: string }[]; + period: ChartPeriod; }) { const { t } = useTranslation(); const [diskChartData, setDiskChartData] = useState([] as diskChartData[]); @@ -778,9 +1117,33 @@ function DiskChart({ const { disk } = formatNezhaInfo(now, data); + const transformDiskData = useMemo( + () => (timestamp: number, value: number) => { + // Convert bytes to percentage + const diskPercent = + data.host.disk_total > 0 ? (value / data.host.disk_total) * 100 : 0; + return { + timeStamp: timestamp.toString(), + disk: diskPercent, + }; + }, + [data.host.disk_total], + ); + + const { historicalData, isLoading } = useHistoricalData( + data.id, + "disk", + period, + transformDiskData, + ); + // 初始化历史数据 useEffect(() => { - if (!hasInitialized.current && messageHistory.length > 0) { + if ( + period === "realtime" && + !hasInitialized.current && + messageHistory.length > 0 + ) { const historyData = messageHistory .map((msg) => { const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse; @@ -799,11 +1162,19 @@ function DiskChart({ hasInitialized.current = true; setHistoryLoaded(true); } - }, [messageHistory, data.id]); + }, [messageHistory, data.id, period]); + + // Reset when switching to realtime + useEffect(() => { + if (period === "realtime") { + hasInitialized.current = false; + setHistoryLoaded(false); + } + }, [period]); // 修改实时数据更新逻辑 useEffect(() => { - if (data && historyLoaded) { + if (data && historyLoaded && period === "realtime") { const timestamp = Date.now().toString(); setDiskChartData((prevData) => { let newData = [] as diskChartData[]; @@ -821,7 +1192,7 @@ function DiskChart({ return newData; }); } - }, [data, historyLoaded, disk]); + }, [data, historyLoaded, disk, period]); const chartConfig = { disk: { @@ -829,6 +1200,8 @@ function DiskChart({ }, } satisfies ChartConfig; + const displayData = period === "realtime" ? diskChartData : historicalData; + return ( - - - formatRelativeTime(value)} - /> - `${value}%`} - /> - - + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + + + formatRelativeTime(value)} + /> + `${value}%`} + /> + + + )} @@ -909,10 +1290,12 @@ function NetworkChart({ now, data, messageHistory, + period, }: { now: number; data: NezhaServer; messageHistory: { data: string }[]; + period: ChartPeriod; }) { const { t } = useTranslation(); const [networkChartData, setNetworkChartData] = useState( @@ -928,9 +1311,59 @@ function NetworkChart({ const { up, down } = formatNezhaInfo(now, data); + // For network, we fetch upload and download separately and combine them + const [networkHistoricalData, setNetworkHistoricalData] = useState< + networkChartData[] + >([]); + const [isLoadingNetwork, setIsLoadingNetwork] = useState(false); + + useEffect(() => { + if (period === "realtime") { + setNetworkHistoricalData([]); + return; + } + + const fetchNetworkData = async () => { + setIsLoadingNetwork(true); + try { + const [uploadResponse, downloadResponse] = await Promise.all([ + fetchServerMetrics(data.id, "net_out_speed", period as MetricPeriod), + fetchServerMetrics(data.id, "net_in_speed", period as MetricPeriod), + ]); + + if (uploadResponse.success && uploadResponse.data?.data_points) { + const downloadMap = new Map(); + if (downloadResponse.success && downloadResponse.data?.data_points) { + for (const point of downloadResponse.data.data_points) { + // Convert bytes to MB + downloadMap.set(point.ts, point.value / 1024 / 1024); + } + } + + const combinedData = uploadResponse.data.data_points.map((point) => ({ + timeStamp: point.ts.toString(), + upload: point.value / 1024 / 1024, // Convert bytes to MB + download: downloadMap.get(point.ts) || 0, + })); + setNetworkHistoricalData(combinedData); + } + } catch (error) { + console.error("Failed to fetch network metrics:", error); + } finally { + setIsLoadingNetwork(false); + } + }; + + fetchNetworkData(); + }, [data.id, period]); + // 初始化历史数据 useEffect(() => { - if (!hasInitialized.current && messageHistory.length > 0) { + if ( + period === "realtime" && + !hasInitialized.current && + messageHistory.length > 0 + ) { const historyData = messageHistory .map((msg) => { const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse; @@ -950,11 +1383,19 @@ function NetworkChart({ hasInitialized.current = true; setHistoryLoaded(true); } - }, [messageHistory, data.id]); + }, [messageHistory, data.id, period]); + + // Reset when switching to realtime + useEffect(() => { + if (period === "realtime") { + hasInitialized.current = false; + setHistoryLoaded(false); + } + }, [period]); // 修改实时数据更新逻辑 useEffect(() => { - if (data && historyLoaded) { + if (data && historyLoaded && period === "realtime") { const timestamp = Date.now().toString(); setNetworkChartData((prevData) => { let newData = [] as networkChartData[]; @@ -975,9 +1416,12 @@ function NetworkChart({ return newData; }); } - }, [data, historyLoaded, down, up]); + }, [data, historyLoaded, down, up, period]); - let maxDownload = Math.max(...networkChartData.map((item) => item.download)); + const displayData = + period === "realtime" ? networkChartData : networkHistoricalData; + + let maxDownload = Math.max(...displayData.map((item) => item.download)); maxDownload = Math.ceil(maxDownload); if (maxDownload < 1) { maxDownload = 1; @@ -1007,7 +1451,7 @@ function NetworkChart({ {t("serverDetailChart.upload")}

- +

{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` @@ -1022,7 +1466,7 @@ function NetworkChart({ {t("serverDetailChart.download")}

- +

{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` @@ -1038,53 +1482,61 @@ function NetworkChart({ config={chartConfig} className="aspect-auto h-[130px] w-full" > - - - formatRelativeTime(value)} - /> - `${value.toFixed(0)}M/s`} - /> - - - + {isLoadingNetwork ? ( +

+ + Loading... + +
+ ) : ( + + + formatRelativeTime(value)} + /> + `${value.toFixed(0)}M/s`} + /> + + + + )} @@ -1096,10 +1548,12 @@ function ConnectChart({ now, data, messageHistory, + period, }: { now: number; data: NezhaServer; messageHistory: { data: string }[]; + period: ChartPeriod; }) { const [connectChartData, setConnectChartData] = useState( [] as connectChartData[], @@ -1114,9 +1568,58 @@ function ConnectChart({ const { tcp, udp } = formatNezhaInfo(now, data); + // For connections, we fetch TCP and UDP separately and combine them + const [connectHistoricalData, setConnectHistoricalData] = useState< + connectChartData[] + >([]); + const [isLoadingConnect, setIsLoadingConnect] = useState(false); + + useEffect(() => { + if (period === "realtime") { + setConnectHistoricalData([]); + return; + } + + const fetchConnectData = async () => { + setIsLoadingConnect(true); + try { + const [tcpResponse, udpResponse] = await Promise.all([ + fetchServerMetrics(data.id, "tcp_conn", period as MetricPeriod), + fetchServerMetrics(data.id, "udp_conn", period as MetricPeriod), + ]); + + if (tcpResponse.success && tcpResponse.data?.data_points) { + const udpMap = new Map(); + if (udpResponse.success && udpResponse.data?.data_points) { + for (const point of udpResponse.data.data_points) { + udpMap.set(point.ts, point.value); + } + } + + const combinedData = tcpResponse.data.data_points.map((point) => ({ + timeStamp: point.ts.toString(), + tcp: point.value, + udp: udpMap.get(point.ts) || 0, + })); + setConnectHistoricalData(combinedData); + } + } catch (error) { + console.error("Failed to fetch connection metrics:", error); + } finally { + setIsLoadingConnect(false); + } + }; + + fetchConnectData(); + }, [data.id, period]); + // 初始化历史数据 useEffect(() => { - if (!hasInitialized.current && messageHistory.length > 0) { + if ( + period === "realtime" && + !hasInitialized.current && + messageHistory.length > 0 + ) { const historyData = messageHistory .map((msg) => { const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse; @@ -1136,11 +1639,19 @@ function ConnectChart({ hasInitialized.current = true; setHistoryLoaded(true); } - }, [messageHistory, data.id]); + }, [messageHistory, data.id, period]); + + // Reset when switching to realtime + useEffect(() => { + if (period === "realtime") { + hasInitialized.current = false; + setHistoryLoaded(false); + } + }, [period]); // 修改实时数据更新逻辑 useEffect(() => { - if (data && historyLoaded) { + if (data && historyLoaded && period === "realtime") { const timestamp = Date.now().toString(); setConnectChartData((prevData) => { let newData = [] as connectChartData[]; @@ -1158,7 +1669,7 @@ function ConnectChart({ return newData; }); } - }, [data, historyLoaded, tcp, udp]); + }, [data, historyLoaded, tcp, udp, period]); const chartConfig = { tcp: { @@ -1169,6 +1680,9 @@ function ConnectChart({ }, } satisfies ChartConfig; + const displayData = + period === "realtime" ? connectChartData : connectHistoricalData; + return (

TCP

- +

{tcp}

UDP

- +

{udp}

@@ -1199,50 +1713,58 @@ function ConnectChart({ config={chartConfig} className="aspect-auto h-[130px] w-full" > - - - formatRelativeTime(value)} - /> - - - - + {isLoadingConnect ? ( +
+ + Loading... + +
+ ) : ( + + + formatRelativeTime(value)} + /> + + + + + )} diff --git a/src/lib/nezha-api.ts b/src/lib/nezha-api.ts index 92d526e..02526c2 100644 --- a/src/lib/nezha-api.ts +++ b/src/lib/nezha-api.ts @@ -1,7 +1,10 @@ import type { LoginUserResponse, + MetricPeriod, + MetricType, MonitorResponse, ServerGroupResponse, + ServerMetricsResponse, ServiceResponse, SettingResponse, } from "@/types/nezha-api"; @@ -69,3 +72,19 @@ export const fetchSetting = async (): Promise => { } return data; }; + +export const fetchServerMetrics = async ( + server_id: number, + metric: MetricType, + period?: MetricPeriod, +): Promise => { + const query = period + ? `?metric=${metric}&period=${period}` + : `?metric=${metric}`; + const response = await fetch(`/api/v1/server/${server_id}/metrics${query}`); + const data = await response.json(); + if (data.error) { + throw new Error(data.error); + } + return data; +}; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9077663..2ec5fb7 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -77,7 +77,11 @@ "mem": "Mem", "swap": "Swap", "upload": "Upload", - "download": "Download" + "download": "Download", + "realtime": "Realtime", + "period1d": "1 Day", + "period7d": "7 Days", + "period30d": "30 Days" }, "footer": { "themeBy": "Theme by " diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 7e19a51..d8f9690 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -77,7 +77,11 @@ "mem": "内存", "swap": "虚拟内存", "upload": "上传", - "download": "下载" + "download": "下载", + "realtime": "实时", + "period1d": "1 天", + "period7d": "7 天", + "period30d": "30 天" }, "footer": { "themeBy": "主题-" diff --git a/src/pages/ServerDetail.tsx b/src/pages/ServerDetail.tsx index e96aed2..959b55e 100644 --- a/src/pages/ServerDetail.tsx +++ b/src/pages/ServerDetail.tsx @@ -3,7 +3,6 @@ import { useNavigate, useParams } from "react-router-dom"; import { NetworkChart } from "@/components/NetworkChart"; import ServerDetailChart from "@/components/ServerDetailChart"; import ServerDetailOverview from "@/components/ServerDetailOverview"; -import ServerDetailSummary from "@/components/ServerDetailSummary"; import TabSwitch from "@/components/TabSwitch"; import { Separator } from "@/components/ui/separator"; @@ -39,9 +38,9 @@ export default function ServerDetail() { -
+ {/*
-
+
*/}
diff --git a/src/types/nezha-api.ts b/src/types/nezha-api.ts index 96469b9..de348c6 100644 --- a/src/types/nezha-api.ts +++ b/src/types/nezha-api.ts @@ -158,3 +158,41 @@ export interface SettingResponse { version: string; }; } + +export type MetricType = + | "cpu" + | "memory" + | "swap" + | "disk" + | "net_in_speed" + | "net_out_speed" + | "net_in_transfer" + | "net_out_transfer" + | "load1" + | "load5" + | "load15" + | "tcp_conn" + | "udp_conn" + | "process_count" + | "temperature" + | "uptime" + | "gpu"; + +export type MetricPeriod = "1d" | "7d" | "30d"; + +export interface MetricDataPoint { + ts: number; + value: number; +} + +export interface ServerMetricsData { + server_id: number; + server_name: string; + metric: string; + data_points: MetricDataPoint[]; +} + +export interface ServerMetricsResponse { + success: boolean; + data: ServerMetricsData; +}