mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-05-06 13:58:43 +00:00
feat: enhance ServerDetailChart with new chart tooltips and sync functionality
This commit is contained in:
@@ -11,11 +11,21 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
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 { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||||
import { formatBytes } from "@/lib/format";
|
import { formatBytes } from "@/lib/format";
|
||||||
import { fetchLoginUser, fetchServerMetrics } from "@/lib/nezha-api";
|
import { fetchLoginUser, fetchServerMetrics } from "@/lib/nezha-api";
|
||||||
import { cn, formatNezhaInfo, formatRelativeTime } from "@/lib/utils";
|
import {
|
||||||
|
cn,
|
||||||
|
formatNezhaInfo,
|
||||||
|
formatRelativeTime,
|
||||||
|
formatTime,
|
||||||
|
} from "@/lib/utils";
|
||||||
import type {
|
import type {
|
||||||
MetricPeriod,
|
MetricPeriod,
|
||||||
NezhaServer,
|
NezhaServer,
|
||||||
@@ -84,7 +94,7 @@ function PeriodSelector({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1 mb-3 flex-wrap">
|
<div className="flex gap-1 mb-3 flex-wrap -mt-5">
|
||||||
{periods.map((period) => {
|
{periods.map((period) => {
|
||||||
// Only realtime and 1d are available for non-logged-in users
|
// Only realtime and 1d are available for non-logged-in users
|
||||||
const isLocked =
|
const isLocked =
|
||||||
@@ -427,6 +437,7 @@ function GpuChart({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AreaChart
|
<AreaChart
|
||||||
|
syncId="serverDetailCharts"
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={{
|
margin={{
|
||||||
@@ -453,6 +464,27 @@ function GpuChart({
|
|||||||
domain={[0, 100]}
|
domain={[0, 100]}
|
||||||
tickFormatter={(value) => `${value}%`}
|
tickFormatter={(value) => `${value}%`}
|
||||||
/>
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
isAnimationActive={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
indicator="dot"
|
||||||
|
labelFormatter={(_, payload) => {
|
||||||
|
return formatTime(
|
||||||
|
Number(payload[0]?.payload?.timeStamp),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
formatter={(value) => (
|
||||||
|
<div className="flex flex-1 items-center justify-between leading-none">
|
||||||
|
<span className="text-muted-foreground">GPU</span>
|
||||||
|
<span className="ml-2 font-medium text-foreground tabular-nums">
|
||||||
|
{Number(value).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Area
|
<Area
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey="gpu"
|
dataKey="gpu"
|
||||||
@@ -607,6 +639,7 @@ function CpuChart({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AreaChart
|
<AreaChart
|
||||||
|
syncId="serverDetailCharts"
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={{
|
margin={{
|
||||||
@@ -633,6 +666,27 @@ function CpuChart({
|
|||||||
domain={[0, 100]}
|
domain={[0, 100]}
|
||||||
tickFormatter={(value) => `${value}%`}
|
tickFormatter={(value) => `${value}%`}
|
||||||
/>
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
isAnimationActive={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
indicator="dot"
|
||||||
|
labelFormatter={(_, payload) => {
|
||||||
|
return formatTime(
|
||||||
|
Number(payload[0]?.payload?.timeStamp),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
formatter={(value) => (
|
||||||
|
<div className="flex flex-1 items-center justify-between leading-none">
|
||||||
|
<span className="text-muted-foreground">CPU</span>
|
||||||
|
<span className="ml-2 font-medium text-foreground tabular-nums">
|
||||||
|
{Number(value).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Area
|
<Area
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey="cpu"
|
dataKey="cpu"
|
||||||
@@ -783,6 +837,7 @@ function ProcessChart({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AreaChart
|
<AreaChart
|
||||||
|
syncId="serverDetailCharts"
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={{
|
margin={{
|
||||||
@@ -807,6 +862,29 @@ function ProcessChart({
|
|||||||
mirror={true}
|
mirror={true}
|
||||||
tickMargin={-15}
|
tickMargin={-15}
|
||||||
/>
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
isAnimationActive={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
indicator={"dot"}
|
||||||
|
labelFormatter={(_, payload) => {
|
||||||
|
return formatTime(
|
||||||
|
Number(payload[0]?.payload?.timeStamp),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
formatter={(value) => (
|
||||||
|
<div className="flex flex-1 items-center justify-between leading-none">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("serverDetailChart.process")}
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 font-medium text-foreground tabular-nums">
|
||||||
|
{Number(value).toFixed(0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Area
|
<Area
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey="process"
|
dataKey="process"
|
||||||
@@ -1043,6 +1121,7 @@ function MemChart({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AreaChart
|
<AreaChart
|
||||||
|
syncId="serverDetailCharts"
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={{
|
margin={{
|
||||||
@@ -1069,6 +1148,35 @@ function MemChart({
|
|||||||
domain={[0, 100]}
|
domain={[0, 100]}
|
||||||
tickFormatter={(value) => `${value}%`}
|
tickFormatter={(value) => `${value}%`}
|
||||||
/>
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
isAnimationActive={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
indicator="dot"
|
||||||
|
labelFormatter={(_, payload) => {
|
||||||
|
return formatTime(
|
||||||
|
Number(payload[0]?.payload?.timeStamp),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
formatter={(value, name) => {
|
||||||
|
const label =
|
||||||
|
name === "mem"
|
||||||
|
? t("serverDetailChart.mem")
|
||||||
|
: t("serverDetailChart.swap");
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 items-center justify-between leading-none">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 font-medium text-foreground tabular-nums">
|
||||||
|
{Number(value).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Area
|
<Area
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey="mem"
|
dataKey="mem"
|
||||||
@@ -1243,6 +1351,7 @@ function DiskChart({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AreaChart
|
<AreaChart
|
||||||
|
syncId="serverDetailCharts"
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={{
|
margin={{
|
||||||
@@ -1269,6 +1378,29 @@ function DiskChart({
|
|||||||
domain={[0, 100]}
|
domain={[0, 100]}
|
||||||
tickFormatter={(value) => `${value}%`}
|
tickFormatter={(value) => `${value}%`}
|
||||||
/>
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
isAnimationActive={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
indicator="dot"
|
||||||
|
labelFormatter={(_, payload) => {
|
||||||
|
return formatTime(
|
||||||
|
Number(payload[0]?.payload?.timeStamp),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
formatter={(value) => (
|
||||||
|
<div className="flex flex-1 items-center justify-between leading-none">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("serverDetailChart.disk")}
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 font-medium text-foreground tabular-nums">
|
||||||
|
{Number(value).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Area
|
<Area
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey="disk"
|
dataKey="disk"
|
||||||
@@ -1490,6 +1622,7 @@ function NetworkChart({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LineChart
|
<LineChart
|
||||||
|
syncId="serverDetailCharts"
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={{
|
margin={{
|
||||||
@@ -1519,6 +1652,35 @@ function NetworkChart({
|
|||||||
domain={[1, maxDownload]}
|
domain={[1, maxDownload]}
|
||||||
tickFormatter={(value) => `${value.toFixed(0)}M/s`}
|
tickFormatter={(value) => `${value.toFixed(0)}M/s`}
|
||||||
/>
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
isAnimationActive={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
indicator="dot"
|
||||||
|
labelFormatter={(_, payload) => {
|
||||||
|
return formatTime(
|
||||||
|
Number(payload[0]?.payload?.timeStamp),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
formatter={(value, name) => {
|
||||||
|
const label =
|
||||||
|
name === "upload"
|
||||||
|
? t("serverDetailChart.upload")
|
||||||
|
: t("serverDetailChart.download");
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 items-center justify-between leading-none">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 font-medium text-foreground tabular-nums">
|
||||||
|
{Number(value).toFixed(2)} MB/s
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Line
|
<Line
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey="upload"
|
dataKey="upload"
|
||||||
@@ -1721,6 +1883,7 @@ function ConnectChart({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LineChart
|
<LineChart
|
||||||
|
syncId="serverDetailCharts"
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={displayData}
|
data={displayData}
|
||||||
margin={{
|
margin={{
|
||||||
@@ -1747,6 +1910,32 @@ function ConnectChart({
|
|||||||
type="number"
|
type="number"
|
||||||
interval="preserveStartEnd"
|
interval="preserveStartEnd"
|
||||||
/>
|
/>
|
||||||
|
<ChartTooltip
|
||||||
|
isAnimationActive={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
indicator="dot"
|
||||||
|
labelFormatter={(_, payload) => {
|
||||||
|
return formatTime(
|
||||||
|
Number(payload[0]?.payload?.timeStamp),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
formatter={(value, name) => {
|
||||||
|
const label = name === "tcp" ? "TCP" : "UDP";
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 items-center justify-between leading-none">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 font-medium text-foreground tabular-nums">
|
||||||
|
{Number(value).toFixed(0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Line
|
<Line
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey="tcp"
|
dataKey="tcp"
|
||||||
|
|||||||
+55
-20
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as RechartsPrimitive from "recharts";
|
import * as RechartsPrimitive from "recharts";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -6,7 +8,7 @@ import { cn } from "@/lib/utils";
|
|||||||
const THEMES = { light: "", dark: ".dark" } as const;
|
const THEMES = { light: "", dark: ".dark" } as const;
|
||||||
|
|
||||||
export type ChartConfig = {
|
export type ChartConfig = {
|
||||||
[k in string]: {
|
[k: string]: {
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
icon?: React.ComponentType;
|
icon?: React.ComponentType;
|
||||||
} & (
|
} & (
|
||||||
@@ -101,14 +103,26 @@ const ChartTooltip = RechartsPrimitive.Tooltip;
|
|||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
const ChartTooltipContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
React.ComponentProps<"div"> & {
|
||||||
React.ComponentProps<"div"> & {
|
active?: boolean;
|
||||||
hideLabel?: boolean;
|
payload?: any[];
|
||||||
hideIndicator?: boolean;
|
label?: any;
|
||||||
indicator?: "line" | "dot" | "dashed";
|
hideLabel?: boolean;
|
||||||
nameKey?: string;
|
hideIndicator?: boolean;
|
||||||
labelKey?: string;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload.sort((a, b) => {
|
||||||
|
return Number(b.value) - Number(a.value);
|
||||||
|
});
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
"grid min-w-32 items-start gap-1.5 overflow-hidden rounded-sm border border-border/50 bg-stone-100 text-xs dark:bg-stone-900",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!nestLabel ? tooltipLabel : null}
|
{!nestLabel && (
|
||||||
<div className="grid gap-1.5">
|
<div className="mx-auto -mb-1 px-2.5 pt-1">
|
||||||
|
{!nestLabel ? tooltipLabel : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn("grid gap-1.5 bg-white px-2.5 py-1.5 dark:bg-black", {
|
||||||
|
"border-t": !nestLabel,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{payload.map((item, index) => {
|
{payload.map((item, index) => {
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
@@ -236,8 +263,15 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
<span
|
||||||
{item.value.toLocaleString()}
|
className={cn(
|
||||||
|
"ml-2 font-medium text-foreground tabular-nums",
|
||||||
|
payload.length === 1 && "-ml-9",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{typeof item.value === "number"
|
||||||
|
? item.value.toFixed(2).toLocaleString()
|
||||||
|
: item.value}{" "}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -257,11 +291,12 @@ const ChartLegend = RechartsPrimitive.Legend;
|
|||||||
|
|
||||||
const ChartLegendContent = React.forwardRef<
|
const ChartLegendContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> &
|
React.ComponentProps<"div"> & {
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
payload?: any[];
|
||||||
hideIcon?: boolean;
|
verticalAlign?: "top" | "bottom" | "middle";
|
||||||
nameKey?: string;
|
hideIcon?: boolean;
|
||||||
}
|
nameKey?: string;
|
||||||
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||||
@@ -303,7 +338,7 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{itemConfig?.label}
|
{key}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user