mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-14 16:54:24 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 555039e028 | |||
| 7134253b2c | |||
| 58d29b440a | |||
| 6a3a9add08 | |||
| 6d986524f1 | |||
| 92a8fed796 |
@@ -0,0 +1,23 @@
|
||||
# Monitor Nezha Redesign
|
||||
|
||||
## Summary
|
||||
Redesign the monitor overview to resemble Nezha dashboard. Phase 1 introduced ServerCard grid. Phase 2 hides node metrics from the overview and adds a detail view with charts and service monitor latency sparklines.
|
||||
|
||||
## Objectives
|
||||
- [x] Analyze `monitor-view.tsx` layout boundaries.
|
||||
- [x] Create `ServerCard` utilizing `Progress` component.
|
||||
- [x] Render metrics (CPU, RAM, Disk, System Load, Connections, Network speeds).
|
||||
- [x] Implement robust real-time updates and seamless state linkage via existing hooks.
|
||||
- [x] Adjust layout positioning for impact at top-of-page.
|
||||
- [x] Hide node metrics from overview — click node card to enter detail view.
|
||||
- [x] Detail view: back button + node header + realtime KPI cards.
|
||||
- [x] Detail view: node metrics chart (CPU/Memory/Disk/Network/Load/Connections).
|
||||
- [x] Detail view: tunnel traffic chart.
|
||||
- [x] Detail view: service monitors rendered as latency sparkline charts (Nezha-style).
|
||||
- [x] Service monitor cards include status dot, type chip, target, interval, and dropdown actions.
|
||||
|
||||
## Technical Details
|
||||
- Phase 1: Injected a `ServerCard` inline component with progress bars and real-time metrics.
|
||||
- Phase 2: Added `detailNodeId` state for drill-down navigation. Grid view shows only summary bar + clickable server cards. Detail view shows KPI summary cards, node metrics chart (reusing existing recharts setup), tunnel traffic chart, and service monitors as a responsive grid of cards each containing a latency-over-time sparkline chart. Reduced file from 2108 to 1799 lines by consolidating the old overview card into the summary bar.
|
||||
|
||||
## Status: Complete
|
||||
@@ -0,0 +1,7 @@
|
||||
# 046 - Node Card Cleanup & Monitor Redesign
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Remove duplicate system metrics from node cards in `node.tsx` (CPU, memory, upload/download speed, upload/download traffic, disk, load)
|
||||
- [x] 2. Redesign monitor `ServerCard` in `monitor-view.tsx` to match the node card style from `node.tsx`
|
||||
- [x] 3. Make all charts in monitor view update incrementally (streaming) instead of full page reload
|
||||
@@ -823,10 +823,7 @@ const SortableTableRow = ({
|
||||
{formatFlow(getForwardDisplayFlow(forward))}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.status} cursor-pointer hover:underline text-primary font-bold`}
|
||||
onClick={() => copyToClipboard(forward.inPort.toString(), "入口端口")}
|
||||
>
|
||||
<TableCell className={FORWARD_GROUPED_TABLE_COLUMN_CLASS.status}>
|
||||
<div className="flex items-center gap-2.5 whitespace-nowrap">
|
||||
<Switch
|
||||
color="success"
|
||||
|
||||
@@ -21,8 +21,9 @@ export default function MonitorPage() {
|
||||
const [nodesLoading, setNodesLoading] = useState(false);
|
||||
const [nodesError, setNodesError] = useState<string | null>(null);
|
||||
|
||||
const loadNodes = useCallback(async () => {
|
||||
setNodesLoading(true);
|
||||
const loadNodes = useCallback(async (options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
if (!silent) setNodesLoading(true);
|
||||
try {
|
||||
const response = await getMonitorNodes();
|
||||
|
||||
@@ -40,11 +41,11 @@ export default function MonitorPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(response.msg || "加载节点失败");
|
||||
if (!silent) toast.error(response.msg || "加载节点失败");
|
||||
} catch {
|
||||
toast.error("加载节点失败");
|
||||
if (!silent) toast.error("加载节点失败");
|
||||
} finally {
|
||||
setNodesLoading(false);
|
||||
if (!silent) setNodesLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -54,7 +55,7 @@ export default function MonitorPage() {
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
void loadNodes();
|
||||
void loadNodes({ silent: true });
|
||||
}, 30_000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
@@ -86,7 +87,7 @@ export default function MonitorPage() {
|
||||
isLoading={nodesLoading}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={loadNodes}
|
||||
onPress={() => loadNodes()}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />
|
||||
刷新节点
|
||||
|
||||
@@ -683,15 +683,7 @@ export default function NodePage() {
|
||||
};
|
||||
|
||||
// 格式化流量
|
||||
const formatTraffic = (bytes: number): string => {
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
const formatFlow = (bytes: number): string => {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||
@@ -719,17 +711,6 @@ export default function NodePage() {
|
||||
return "未知链路";
|
||||
};
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (
|
||||
value: number,
|
||||
offline = false,
|
||||
): "default" | "primary" | "secondary" | "success" | "warning" | "danger" => {
|
||||
if (offline) return "default";
|
||||
if (value <= 50) return "success";
|
||||
if (value <= 80) return "warning";
|
||||
|
||||
return "danger";
|
||||
};
|
||||
|
||||
// IPv4/IPv6 格式验证(仅用于判定地址族)
|
||||
const ipv4Regex =
|
||||
@@ -2162,235 +2143,7 @@ export default function NodePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isRemoteNode && (
|
||||
<>
|
||||
{/* 系统监控 */}
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>CPU</span>
|
||||
<span className="font-mono">
|
||||
{node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? `${node.systemInfo.cpuUsage.toFixed(1)}%`
|
||||
: "-"}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
aria-label="CPU使用率"
|
||||
color={getProgressColor(
|
||||
node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? node.systemInfo.cpuUsage
|
||||
: 0,
|
||||
node.connectionStatus !== "online",
|
||||
)}
|
||||
size="sm"
|
||||
value={
|
||||
node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? node.systemInfo.cpuUsage
|
||||
: 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>内存</span>
|
||||
<span className="font-mono">
|
||||
{node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? `${node.systemInfo.memoryUsage.toFixed(1)}%`
|
||||
: "-"}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
aria-label="内存使用率"
|
||||
color={getProgressColor(
|
||||
node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? node.systemInfo.memoryUsage
|
||||
: 0,
|
||||
node.connectionStatus !== "online",
|
||||
)}
|
||||
size="sm"
|
||||
value={
|
||||
node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? node.systemInfo.memoryUsage
|
||||
: 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="text-center p-2 bg-default-50 dark:bg-default-100 rounded">
|
||||
<div className="text-default-600 mb-0.5">
|
||||
上传
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
{node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? formatSpeed(
|
||||
node.systemInfo.uploadSpeed,
|
||||
)
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-default-50 dark:bg-default-100 rounded">
|
||||
<div className="text-default-600 mb-0.5">
|
||||
下载
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
{node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? formatSpeed(
|
||||
node.systemInfo.downloadSpeed,
|
||||
)
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 流量统计 */}
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="text-center p-2 bg-primary-50 dark:bg-primary-100/20 rounded border border-primary-200 dark:border-primary-300/20">
|
||||
<div className="text-primary-600 dark:text-primary-400 mb-0.5">
|
||||
↑ 上行流量
|
||||
</div>
|
||||
<div className="font-mono text-primary-700 dark:text-primary-300">
|
||||
{node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? formatTraffic(
|
||||
node.systemInfo.uploadTraffic,
|
||||
)
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center p-2 bg-success-50 dark:bg-success-100/20 rounded border border-success-200 dark:border-success-300/20">
|
||||
<div className="text-success-600 dark:text-success-400 mb-0.5">
|
||||
↓ 下行流量
|
||||
</div>
|
||||
<div className="font-mono text-success-700 dark:text-success-300">
|
||||
{node.connectionStatus === "online" &&
|
||||
node.systemInfo
|
||||
? formatTraffic(
|
||||
node.systemInfo.downloadTraffic,
|
||||
)
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 扩展指标:磁盘/负载/连接 */}
|
||||
{node.connectionStatus === "online" &&
|
||||
node.systemInfo && (
|
||||
<div className="space-y-2">
|
||||
{node.systemInfo.diskUsage !==
|
||||
undefined && (
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>磁盘</span>
|
||||
<span className="font-mono">
|
||||
{node.systemInfo.diskUsage.toFixed(
|
||||
1,
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
aria-label="磁盘使用率"
|
||||
color={getProgressColor(
|
||||
node.systemInfo.diskUsage,
|
||||
false,
|
||||
)}
|
||||
size="sm"
|
||||
value={node.systemInfo.diskUsage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(node.systemInfo.load1 !== undefined ||
|
||||
node.systemInfo.load5 !== undefined ||
|
||||
node.systemInfo.load15 !==
|
||||
undefined) && (
|
||||
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||
{node.systemInfo.load1 !==
|
||||
undefined && (
|
||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100 rounded">
|
||||
<div className="text-default-500 text-[10px]">
|
||||
负载 1m
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
{node.systemInfo.load1.toFixed(
|
||||
2,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{node.systemInfo.load5 !==
|
||||
undefined && (
|
||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100 rounded">
|
||||
<div className="text-default-500 text-[10px]">
|
||||
负载 5m
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
{node.systemInfo.load5.toFixed(
|
||||
2,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{node.systemInfo.load15 !==
|
||||
undefined && (
|
||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100 rounded">
|
||||
<div className="text-default-500 text-[10px]">
|
||||
负载 15m
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
{node.systemInfo.load15.toFixed(
|
||||
2,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(node.systemInfo.tcpConns !==
|
||||
undefined ||
|
||||
node.systemInfo.udpConns !==
|
||||
undefined) && (
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{node.systemInfo.tcpConns !==
|
||||
undefined && (
|
||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100 rounded">
|
||||
<div className="text-default-500 text-[10px]">
|
||||
TCP 连接
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
{node.systemInfo.tcpConns}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{node.systemInfo.udpConns !==
|
||||
undefined && (
|
||||
<div className="text-center p-1.5 bg-default-50 dark:bg-default-100 rounded">
|
||||
<div className="text-default-500 text-[10px]">
|
||||
UDP 连接
|
||||
</div>
|
||||
<div className="font-mono">
|
||||
{node.systemInfo.udpConns}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-auto space-y-3">
|
||||
{/* 操作按钮 */}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user