-
+
{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
@@ -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;
+}