From aae837c11000b267d5c173baf2d0da4b7f032d28 Mon Sep 17 00:00:00 2001 From: hamster1963 <1410514192@qq.com> Date: Sun, 24 Nov 2024 02:51:41 +0800 Subject: [PATCH] feat: detail chart --- bun.lockb | Bin 145938 -> 159867 bytes eslint.config.js | 1 + package.json | 3 +- src/components/ServerDetailChart.tsx | 742 ++++++++++++++++++ src/components/ServerDetailOverview.tsx | 171 ++++ .../loading/ServerDetailLoading.tsx | 1 - .../ui/animated-circular-progress-bar.tsx | 107 +++ src/components/ui/badge.tsx | 14 +- src/components/ui/chart.tsx | 363 +++++++++ src/pages/ServerDetail.tsx | 172 +--- 10 files changed, 1398 insertions(+), 176 deletions(-) create mode 100644 src/components/ServerDetailChart.tsx create mode 100644 src/components/ServerDetailOverview.tsx create mode 100644 src/components/ui/animated-circular-progress-bar.tsx create mode 100644 src/components/ui/chart.tsx diff --git a/bun.lockb b/bun.lockb index e282bb92366a6fcd1ba02bc2fd75a6bf63ca7255..4c2db6cc1924eecdeaf4663b03051e62032688f4 100755 GIT binary patch delta 29015 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 delta 20971 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 diff --git a/eslint.config.js b/eslint.config.js index 79a552e..c1d602f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,7 @@ export default tseslint.config( "warn", { allowConstantExport: true }, ], + "react-hooks/exhaustive-deps": "off", }, }, ); diff --git a/package.json b/package.json index c857601..fb2182d 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,13 @@ "clsx": "^2.1.1", "country-flag-icons": "^1.5.13", "framer-motion": "^11.11.17", - "lucide-react": "^0.453.0", + "lucide-react": "^0.460.0", "luxon": "^3.5.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.28.0", "react-use-websocket": "^4.11.1", + "recharts": "^2.13.3", "sonner": "^1.7.0", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7" diff --git a/src/components/ServerDetailChart.tsx b/src/components/ServerDetailChart.tsx new file mode 100644 index 0000000..70cec45 --- /dev/null +++ b/src/components/ServerDetailChart.tsx @@ -0,0 +1,742 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { ChartConfig, ChartContainer } from "@/components/ui/chart"; +import { formatNezhaInfo, formatRelativeTime } from "@/lib/utils"; +import { NezhaAPI, NezhaAPIResponse } from "@/types/nezha-api"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import useWebSocket from "react-use-websocket"; +import { + Area, + AreaChart, + CartesianGrid, + Line, + LineChart, + XAxis, + YAxis, +} from "recharts"; +import { ServerDetailChartLoading } from "./loading/ServerDetailLoading"; +import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"; + +type cpuChartData = { + timeStamp: string; + cpu: number; +}; + +type processChartData = { + timeStamp: string; + process: number; +}; + +type diskChartData = { + timeStamp: string; + disk: number; +}; + +type memChartData = { + timeStamp: string; + mem: number; + swap: number; +}; + +type networkChartData = { + timeStamp: string; + upload: number; + download: number; +}; + +type connectChartData = { + timeStamp: string; + tcp: number; + udp: number; +}; + +export default function ServerDetailChart() { + const { id } = useParams(); + const { lastMessage, readyState } = useWebSocket("/api/v1/ws/server", { + shouldReconnect: () => true, + reconnectInterval: 3000, + }); + + // 检查连接状态 + if (readyState !== 1) { + return ( +
+

connecting...

+
+ ); + } + + // 解析消息 + const nezhaWsData = lastMessage + ? (JSON.parse(lastMessage.data) as NezhaAPIResponse) + : null; + + if (!nezhaWsData) { + return ; + } + + const server = nezhaWsData.servers.find((s) => s.id === Number(id)); + + if (!server) { + return ; + } + + return ( +
+ + + + + + +
+ ); +} + +function CpuChart({ data }: { data: NezhaAPI }) { + const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[]); + + const { cpu } = formatNezhaInfo(data); + + useEffect(() => { + if (data) { + const timestamp = Date.now().toString(); + let newData = [] as cpuChartData[]; + if (cpuChartData.length === 0) { + newData = [ + { timeStamp: timestamp, cpu: cpu }, + { timeStamp: timestamp, cpu: cpu }, + ]; + } else { + newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }]; + } + if (newData.length > 30) { + newData.shift(); + } + setCpuChartData(newData); + } + }, [data]); + + const chartConfig = { + cpu: { + label: "CPU", + }, + } satisfies ChartConfig; + + return ( + + +
+
+

CPU

+
+

+ {cpu.toFixed(0)}% +

+ +
+
+ + + + formatRelativeTime(value)} + /> + `${value}%`} + /> + + + +
+
+
+ ); +} + +function ProcessChart({ data }: { data: NezhaAPI }) { + const [processChartData, setProcessChartData] = useState( + [] as processChartData[], + ); + + const { process } = formatNezhaInfo(data); + + useEffect(() => { + if (data) { + const timestamp = Date.now().toString(); + let newData = [] as processChartData[]; + if (processChartData.length === 0) { + newData = [ + { timeStamp: timestamp, process: process }, + { timeStamp: timestamp, process: process }, + ]; + } else { + newData = [ + ...processChartData, + { timeStamp: timestamp, process: process }, + ]; + } + if (newData.length > 30) { + newData.shift(); + } + setProcessChartData(newData); + } + }, [data]); + + const chartConfig = { + process: { + label: "Process", + }, + } satisfies ChartConfig; + + return ( + + +
+
+

{"Process"}

+
+

{process}

+
+
+ + + + formatRelativeTime(value)} + /> + + + + +
+
+
+ ); +} + +function MemChart({ data }: { data: NezhaAPI }) { + const [memChartData, setMemChartData] = useState([] as memChartData[]); + + const { mem, swap } = formatNezhaInfo(data); + + useEffect(() => { + if (data) { + const timestamp = Date.now().toString(); + let newData = [] as memChartData[]; + if (memChartData.length === 0) { + newData = [ + { timeStamp: timestamp, mem: mem, swap: swap }, + { timeStamp: timestamp, mem: mem, swap: swap }, + ]; + } else { + newData = [ + ...memChartData, + { timeStamp: timestamp, mem: mem, swap: swap }, + ]; + } + if (newData.length > 30) { + newData.shift(); + } + setMemChartData(newData); + } + }, [data]); + + const chartConfig = { + mem: { + label: "Mem", + }, + swap: { + label: "Swap", + }, + } satisfies ChartConfig; + + return ( + + +
+
+
+
+

{"Mem"}

+
+ +

{mem.toFixed(0)}%

+
+
+
+

{"Swap"}

+
+ +

{swap.toFixed(0)}%

+
+
+
+
+ + + + formatRelativeTime(value)} + /> + `${value}%`} + /> + + + + +
+
+
+ ); +} + +function DiskChart({ data }: { data: NezhaAPI }) { + const [diskChartData, setDiskChartData] = useState([] as diskChartData[]); + + const { disk } = formatNezhaInfo(data); + + useEffect(() => { + if (data) { + const timestamp = Date.now().toString(); + let newData = [] as diskChartData[]; + if (diskChartData.length === 0) { + newData = [ + { timeStamp: timestamp, disk: disk }, + { timeStamp: timestamp, disk: disk }, + ]; + } else { + newData = [...diskChartData, { timeStamp: timestamp, disk: disk }]; + } + if (newData.length > 30) { + newData.shift(); + } + setDiskChartData(newData); + } + }, [data]); + + const chartConfig = { + disk: { + label: "Disk", + }, + } satisfies ChartConfig; + + return ( + + +
+
+

{"Disk"}

+
+

+ {disk.toFixed(0)}% +

+ +
+
+ + + + formatRelativeTime(value)} + /> + `${value}%`} + /> + + + +
+
+
+ ); +} + +function NetworkChart({ data }: { data: NezhaAPI }) { + const [networkChartData, setNetworkChartData] = useState( + [] as networkChartData[], + ); + + const { up, down } = formatNezhaInfo(data); + + useEffect(() => { + if (data) { + const timestamp = Date.now().toString(); + let newData = [] as networkChartData[]; + if (networkChartData.length === 0) { + newData = [ + { timeStamp: timestamp, upload: up, download: down }, + { timeStamp: timestamp, upload: up, download: down }, + ]; + } else { + newData = [ + ...networkChartData, + { timeStamp: timestamp, upload: up, download: down }, + ]; + } + if (newData.length > 30) { + newData.shift(); + } + setNetworkChartData(newData); + } + }, [data]); + + let maxDownload = Math.max(...networkChartData.map((item) => item.download)); + maxDownload = Math.ceil(maxDownload); + if (maxDownload < 1) { + maxDownload = 1; + } + + const chartConfig = { + upload: { + label: "Upload", + }, + download: { + label: "Download", + }, + } satisfies ChartConfig; + + return ( + + +
+
+
+
+

{"Upload"}

+
+ +

{up.toFixed(2)} M/s

+
+
+
+

{"Download"}

+
+ +

{down.toFixed(2)} M/s

+
+
+
+
+ + + + formatRelativeTime(value)} + /> + `${value.toFixed(0)}M/s`} + /> + + + + +
+
+
+ ); +} + +function ConnectChart({ data }: { data: NezhaAPI }) { + const [connectChartData, setConnectChartData] = useState( + [] as connectChartData[], + ); + + const { tcp, udp } = formatNezhaInfo(data); + + useEffect(() => { + if (data) { + const timestamp = Date.now().toString(); + let newData = [] as connectChartData[]; + if (connectChartData.length === 0) { + newData = [ + { timeStamp: timestamp, tcp: tcp, udp: udp }, + { timeStamp: timestamp, tcp: tcp, udp: udp }, + ]; + } else { + newData = [ + ...connectChartData, + { timeStamp: timestamp, tcp: tcp, udp: udp }, + ]; + } + if (newData.length > 30) { + newData.shift(); + } + setConnectChartData(newData); + } + }, [data]); + + const chartConfig = { + tcp: { + label: "TCP", + }, + udp: { + label: "UDP", + }, + } satisfies ChartConfig; + + return ( + + +
+
+
+
+

TCP

+
+ +

{tcp}

+
+
+
+

UDP

+
+ +

{udp}

+
+
+
+
+ + + + formatRelativeTime(value)} + /> + + + + + +
+
+
+ ); +} diff --git a/src/components/ServerDetailOverview.tsx b/src/components/ServerDetailOverview.tsx new file mode 100644 index 0000000..35cf6f2 --- /dev/null +++ b/src/components/ServerDetailOverview.tsx @@ -0,0 +1,171 @@ +import { BackIcon } from "@/components/Icon"; +import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"; +import ServerFlag from "@/components/ServerFlag"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "@/components/ui/card"; +import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils"; +import { NezhaAPIResponse } from "@/types/nezha-api"; +import { useNavigate, useParams } from "react-router-dom"; +import useWebSocket from "react-use-websocket"; + +export default function ServerDetailOverview() { + const navigate = useNavigate(); + const { id } = useParams(); + const { lastMessage, readyState } = useWebSocket("/api/v1/ws/server", { + shouldReconnect: () => true, + reconnectInterval: 3000, + }); + + // 检查连接状态 + if (readyState !== 1) { + return ( +
+

connecting...

+
+ ); + } + + // 解析消息 + const nezhaWsData = lastMessage + ? (JSON.parse(lastMessage.data) as NezhaAPIResponse) + : null; + + if (!nezhaWsData) { + return ; + } + + const server = nezhaWsData.servers.find((s) => s.id === Number(id)); + + if (!server) { + return ; + } + + const { name, online, uptime, version } = formatNezhaInfo(server); + + return ( +
+
navigate("/")} + className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" + > + + {name} +
+
+ + +
+

{"Status"}

+ + {online ? "Online" : "Offline"} + +
+
+
+ + +
+

{"Uptime"}

+
+ {" "} + {online ? (uptime / 86400).toFixed(0) : "N/A"} {"Days"}{" "} +
+
+
+
+ + +
+

{"Version"}

+
{version || "Unknown"}
+
+
+
+ + +
+

{"Arch"}

+
{server.host.arch || "Unknown"}
+
+
+
+ + +
+

{"Mem"}

+
+ {formatBytes(server.host.mem_total)} +
+
+
+
+ + +
+

{"Disk"}

+
+ {formatBytes(server.host.disk_total)} +
+
+
+
+ + +
+

{"Region"}

+
+
+ {server.host.country_code?.toUpperCase() || "Unknown"} +
+ {server.host.country_code && ( + + )} +
+
+
+
+
+
+ + +
+

{"System"}

+ {server.host.platform ? ( +
+ {" "} + {server.host.platform || "Unknown"} -{" "} + {server.host.platform_version}{" "} +
+ ) : ( +
Unknown
+ )} +
+
+
+ + +
+

{"CPU"}

+ {server.host.cpu ? ( +
{server.host.cpu}
+ ) : ( +
Unknown
+ )} +
+
+
+
+
+ ); +} diff --git a/src/components/loading/ServerDetailLoading.tsx b/src/components/loading/ServerDetailLoading.tsx index 7e19723..5e78fe7 100644 --- a/src/components/loading/ServerDetailLoading.tsx +++ b/src/components/loading/ServerDetailLoading.tsx @@ -1,4 +1,3 @@ - import { Skeleton } from "@/components/ui/skeleton"; import { BackIcon } from "../Icon"; import { useNavigate } from "react-router-dom"; diff --git a/src/components/ui/animated-circular-progress-bar.tsx b/src/components/ui/animated-circular-progress-bar.tsx new file mode 100644 index 0000000..dd96fe8 --- /dev/null +++ b/src/components/ui/animated-circular-progress-bar.tsx @@ -0,0 +1,107 @@ +import { cn } from "@/lib/utils"; + +interface Props { + max: number; + value: number; + min: number; + className?: string; + primaryColor?: string; +} + +export default function AnimatedCircularProgressBar({ + max = 100, + min = 0, + value = 0, + primaryColor, + className, +}: Props) { + const circumference = 2 * Math.PI * 45; + const percentPx = circumference / 100; + const currentPercent = ((value - min) / (max - min)) * 100; + + return ( +
+ + {currentPercent <= 90 && currentPercent >= 0 && ( + + )} + + + + {currentPercent} + +
+ ); +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index f000e3e..d3d5d60 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", @@ -20,8 +20,8 @@ const badgeVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); export interface BadgeProps extends React.HTMLAttributes, @@ -30,7 +30,7 @@ export interface BadgeProps function Badge({ className, variant, ...props }: BadgeProps) { return (
- ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx new file mode 100644 index 0000000..ba084ed --- /dev/null +++ b/src/components/ui/chart.tsx @@ -0,0 +1,363 @@ +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; + +import { cn } from "@/lib/utils"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + + {children} + +
+
+ ); +}); +ChartContainer.displayName = "Chart"; + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color, + ); + + if (!colorConfig.length) { + return null; + } + + return ( +