From db21ce6bb40d2c6e7c7f54a69d1b476d8cc5c263 Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:54:12 +0800 Subject: [PATCH] feat: implement monitor page list view (#345) --- plans/048-release-2-1-9-alpha5.md | 6 +- plans/049-monitor-list-view.md | 32 ++++ vite-frontend/src/pages/monitor.tsx | 33 ++-- vite-frontend/src/pages/node/monitor-view.tsx | 150 +++++++++++++++--- 4 files changed, 189 insertions(+), 32 deletions(-) create mode 100644 plans/049-monitor-list-view.md diff --git a/plans/048-release-2-1-9-alpha5.md b/plans/048-release-2-1-9-alpha5.md index e9add12..efb63de 100644 --- a/plans/048-release-2-1-9-alpha5.md +++ b/plans/048-release-2-1-9-alpha5.md @@ -6,6 +6,6 @@ Release the current state of `main` (including the monitor tab beautification an ## Tasks - [x] Update `AGENTS.md` with current date, branch, and anticipated tag. - [x] Create a release branch `release/2.1.9-alpha5`. -- [ ] Commit and push the branch. -- [ ] Create a Pull Request and merge it into `main`. -- [ ] Publish the new tag `2.1.9-alpha5`. +- [x] Commit and push the branch. +- [x] Create a Pull Request and merge it into `main`. +- [x] Publish the new tag `2.1.9-alpha5`. diff --git a/plans/049-monitor-list-view.md b/plans/049-monitor-list-view.md new file mode 100644 index 0000000..2247844 --- /dev/null +++ b/plans/049-monitor-list-view.md @@ -0,0 +1,32 @@ +# 049 - Monitor List View + +## Objective +The user requested that the Monitor page should switch its card-based grid view to a list view similar to a provided screenshot, and a view mode toggle should be added to the top right. + +## Expected Features +1. View Mode Toggle + - Add a state `viewMode` in `pages/monitor.tsx`. + - Add a toggle button with LayoutGrid/List icons next to the refresh button. + - Pass `viewMode` down to `MonitorView` component. +2. List View Implementation + - Extend `MonitorViewProps` with `viewMode: "list" | "grid"`. + - Render the `ServerCard` grid when `viewMode === "grid"`. + - Render a `Table` when `viewMode === "list"`. + - The list view should include: + - 状态 (Status: Colored dot depending on `isOnline`). + - 名称 (Name: Node name). + - 速率 (Speed: Up/Down speeds styled appropriately). + - 流量 (Traffic: Total Up/Down bytes). + - 开机时长 (Uptime). + - 连接数 (Connections: TCP/UDP). + - CPU (Progress bar). + - RAM (Progress bar). + - 存储 (Storage / Disk Progress bar). + - 操作 (Actions: Eye view icon to open detailed monitor). + +## Checklist +- [x] Create plan document. +- [x] Add viewMode state and toggle in `monitor.tsx`. +- [x] Receive viewMode in `MonitorView` and selectively render Grid vs List views. +- [x] Ensure list correctly visualizes node info, speed, traffic, uptime, conns, CPU/RAM/Disk usages, and action icons. +- [x] Fix HeroUI missing TableProps typings (remove `removeWrapper` prop). diff --git a/vite-frontend/src/pages/monitor.tsx b/vite-frontend/src/pages/monitor.tsx index fdfdb2e..093e98e 100644 --- a/vite-frontend/src/pages/monitor.tsx +++ b/vite-frontend/src/pages/monitor.tsx @@ -2,7 +2,7 @@ import type { MonitorNodeApiItem } from "@/api/types"; import { useCallback, useEffect, useMemo, useState } from "react"; import toast from "react-hot-toast"; -import { RefreshCw } from "lucide-react"; +import { RefreshCw, LayoutGrid, List } from "lucide-react"; import { AnimatedPage } from "@/components/animated-page"; import { Button } from "@/shadcn-bridge/heroui/button"; @@ -20,6 +20,7 @@ export default function MonitorPage() { const [nodes, setNodes] = useState([]); const [nodesLoading, setNodesLoading] = useState(false); const [nodesError, setNodesError] = useState(null); + const [viewMode, setViewMode] = useState<"list" | "grid">("list"); const loadNodes = useCallback(async (options?: { silent?: boolean }) => { const silent = options?.silent ?? false; @@ -83,15 +84,25 @@ export default function MonitorPage() { 实时节点状态 + 历史指标图表 + 隧道流量 + 服务监控(TCP/ICMP) - +
+ + +
{nodesError ? ( @@ -106,7 +117,7 @@ export default function MonitorPage() { ) : null} - + ); } diff --git a/vite-frontend/src/pages/node/monitor-view.tsx b/vite-frontend/src/pages/node/monitor-view.tsx index 963dd1c..e748ff3 100644 --- a/vite-frontend/src/pages/node/monitor-view.tsx +++ b/vite-frontend/src/pages/node/monitor-view.tsx @@ -29,6 +29,7 @@ import { Server, Clock, ArrowLeft, + Eye, } from "lucide-react"; import toast from "react-hot-toast"; @@ -77,6 +78,7 @@ import { useNodeRealtime } from "@/pages/node/use-node-realtime"; interface MonitorViewProps { nodeMap: Map; + viewMode?: "list" | "grid"; } type RealtimeNodeMetric = { @@ -274,7 +276,7 @@ const DEFAULT_SERVICE_MONITOR_LIMITS: ServiceMonitorLimitsApiData = { maxTimeoutSec: 60, }; -export function MonitorView({ nodeMap }: MonitorViewProps) { +export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) { const [detailNodeId, setDetailNodeId] = useState(null); const [selectedNodeId, setSelectedNodeId] = useState(null); const [metrics, setMetrics] = useState([]); @@ -1170,7 +1172,7 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { )} - {/* ====== GRID VIEW ====== */} + {/* ====== GRID/LIST VIEW ====== */} {!accessDenied && !detailNodeId && ( <>
@@ -1183,22 +1185,134 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { {monitorSummary.stale > 0 && 陈旧 {monitorSummary.stale}}
-
- {nodes.map((node) => { - const metric = realtimeNodeMetrics[node.id] || null; - return ( - { - setDetailNodeId(node.id); - setSelectedNodeId(node.id); - }} - /> - ); - })} -
+ {viewMode === "grid" ? ( +
+ {nodes.map((node) => { + const metric = realtimeNodeMetrics[node.id] || null; + return ( + { + setDetailNodeId(node.id); + setSelectedNodeId(node.id); + }} + /> + ); + })} +
+ ) : ( + + + + 状态 + 名称 + 速率 + 流量 + 开机时长 + 连接数 + CPU + RAM + 存储 + 操作 + + + {nodes.map((node) => { + const metric = realtimeNodeMetrics[node.id] || null; + const isOnline = node.connectionStatus === "online"; + + return ( + + +
+ + + {node.name} + + +
+
+ {isOnline && metric ? formatBytesPerSecond(metric.netOutSpeed) : "-"} ↑ +
+
+ {isOnline && metric ? formatBytesPerSecond(metric.netInSpeed) : "-"} ↓ +
+
+
+ +
+
+ {isOnline && metric ? formatBytes(metric.netOutBytes) : "-"} ↑ +
+
+ {isOnline && metric ? formatBytes(metric.netInBytes) : "-"} ↓ +
+
+
+ + + {isOnline && metric ? formatUptime(metric.uptime) : "-"} + + + +
+
TCP {isOnline && metric ? metric.tcpConns : "-"}
+
UDP {isOnline && metric ? metric.udpConns : "-"}
+
+
+ +
+ {isOnline && metric ? ( + + ) : ( +
+ )} + {isOnline && metric ? `${metric.cpuUsage.toFixed(1)}%` : "-"} +
+ + +
+ {isOnline && metric ? ( + + ) : ( +
+ )} + {isOnline && metric ? `${metric.memoryUsage.toFixed(1)}%` : "-"} +
+ + +
+ {isOnline && metric ? ( + + ) : ( +
+ )} + {isOnline && metric ? `${metric.diskUsage.toFixed(1)}%` : "-"} +
+ + +
+ +
+
+ + ); + })} + +
+
+ )} )}