From 6dae6cce8fdd0f3da96be8105ec979adfa6cfa34 Mon Sep 17 00:00:00 2001
From: hamster1963 <1410514192@qq.com>
Date: Fri, 30 Jan 2026 09:30:50 +0800
Subject: [PATCH] feat: enhance ServerDetailChart with new chart tooltips and
sync functionality
---
src/components/ServerDetailChart.tsx | 195 ++++++++++++++++++++++++++-
src/components/ui/chart.tsx | 75 ++++++++---
2 files changed, 247 insertions(+), 23 deletions(-)
diff --git a/src/components/ServerDetailChart.tsx b/src/components/ServerDetailChart.tsx
index fd9ad4e..be7631b 100644
--- a/src/components/ServerDetailChart.tsx
+++ b/src/components/ServerDetailChart.tsx
@@ -11,11 +11,21 @@ import {
YAxis,
} from "recharts";
import { Card, CardContent } from "@/components/ui/card";
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
+import {
+ type ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} 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 {
+ cn,
+ formatNezhaInfo,
+ formatRelativeTime,
+ formatTime,
+} from "@/lib/utils";
import type {
MetricPeriod,
NezhaServer,
@@ -84,7 +94,7 @@ function PeriodSelector({
];
return (
-
+
{periods.map((period) => {
// Only realtime and 1d are available for non-logged-in users
const isLocked =
@@ -427,6 +437,7 @@ function GpuChart({
) : (
`${value}%`}
/>
+ {
+ return formatTime(
+ Number(payload[0]?.payload?.timeStamp),
+ );
+ }}
+ formatter={(value) => (
+
+ GPU
+
+ {Number(value).toFixed(1)}%
+
+
+ )}
+ />
+ }
+ />
) : (
`${value}%`}
/>
+ {
+ return formatTime(
+ Number(payload[0]?.payload?.timeStamp),
+ );
+ }}
+ formatter={(value) => (
+
+ CPU
+
+ {Number(value).toFixed(1)}%
+
+
+ )}
+ />
+ }
+ />
) : (
+ {
+ return formatTime(
+ Number(payload[0]?.payload?.timeStamp),
+ );
+ }}
+ formatter={(value) => (
+
+
+ {t("serverDetailChart.process")}
+
+
+ {Number(value).toFixed(0)}
+
+
+ )}
+ />
+ }
+ />
) : (
`${value}%`}
/>
+ {
+ return formatTime(
+ Number(payload[0]?.payload?.timeStamp),
+ );
+ }}
+ formatter={(value, name) => {
+ const label =
+ name === "mem"
+ ? t("serverDetailChart.mem")
+ : t("serverDetailChart.swap");
+ return (
+
+
+ {label}
+
+
+ {Number(value).toFixed(1)}%
+
+
+ );
+ }}
+ />
+ }
+ />
) : (
`${value}%`}
/>
+ {
+ return formatTime(
+ Number(payload[0]?.payload?.timeStamp),
+ );
+ }}
+ formatter={(value) => (
+
+
+ {t("serverDetailChart.disk")}
+
+
+ {Number(value).toFixed(1)}%
+
+
+ )}
+ />
+ }
+ />
) : (
`${value.toFixed(0)}M/s`}
/>
+ {
+ return formatTime(
+ Number(payload[0]?.payload?.timeStamp),
+ );
+ }}
+ formatter={(value, name) => {
+ const label =
+ name === "upload"
+ ? t("serverDetailChart.upload")
+ : t("serverDetailChart.download");
+ return (
+
+
+ {label}
+
+
+ {Number(value).toFixed(2)} MB/s
+
+
+ );
+ }}
+ />
+ }
+ />
) : (
+ {
+ return formatTime(
+ Number(payload[0]?.payload?.timeStamp),
+ );
+ }}
+ formatter={(value, name) => {
+ const label = name === "tcp" ? "TCP" : "UDP";
+ return (
+
+
+ {label}
+
+
+ {Number(value).toFixed(0)}
+
+
+ );
+ }}
+ />
+ }
+ />
&
- React.ComponentProps<"div"> & {
- hideLabel?: boolean;
- hideIndicator?: boolean;
- indicator?: "line" | "dot" | "dashed";
- nameKey?: string;
- labelKey?: string;
- }
+ React.ComponentProps<"div"> & {
+ active?: boolean;
+ payload?: any[];
+ label?: any;
+ hideLabel?: boolean;
+ hideIndicator?: boolean;
+ indicator?: "line" | "dot" | "dashed";
+ nameKey?: string;
+ labelKey?: string;
+ labelFormatter?: (value: any, payload: any[]) => React.ReactNode;
+ formatter?: (
+ value: any,
+ name: any,
+ item: any,
+ index: number,
+ payload: any,
+ ) => React.ReactNode;
+ color?: string;
+ labelClassName?: string;
+ }
>(
(
{
@@ -170,18 +184,31 @@ const ChartTooltipContent = React.forwardRef<
return null;
}
+ payload.sort((a, b) => {
+ return Number(b.value) - Number(a.value);
+ });
+
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
- {!nestLabel ? tooltipLabel : null}
-
+ {!nestLabel && (
+
+ {!nestLabel ? tooltipLabel : null}
+
+ )}
+
+
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
@@ -236,8 +263,15 @@ const ChartTooltipContent = React.forwardRef<
{item.value && (
-
- {item.value.toLocaleString()}
+
+ {typeof item.value === "number"
+ ? item.value.toFixed(2).toLocaleString()
+ : item.value}{" "}
)}
@@ -257,11 +291,12 @@ const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
- React.ComponentProps<"div"> &
- Pick
& {
- hideIcon?: boolean;
- nameKey?: string;
- }
+ React.ComponentProps<"div"> & {
+ payload?: any[];
+ verticalAlign?: "top" | "bottom" | "middle";
+ hideIcon?: boolean;
+ nameKey?: string;
+ }
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
@@ -303,7 +338,7 @@ const ChartLegendContent = React.forwardRef<
}}
/>
)}
- {itemConfig?.label}
+ {key}
);
})}