mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-11 03:36:37 +08:00
feat: monitor page redesign and node card cleanup (#340)
This commit is contained in:
@@ -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
|
||||||
@@ -21,8 +21,9 @@ export default function MonitorPage() {
|
|||||||
const [nodesLoading, setNodesLoading] = useState(false);
|
const [nodesLoading, setNodesLoading] = useState(false);
|
||||||
const [nodesError, setNodesError] = useState<string | null>(null);
|
const [nodesError, setNodesError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadNodes = useCallback(async () => {
|
const loadNodes = useCallback(async (options?: { silent?: boolean }) => {
|
||||||
setNodesLoading(true);
|
const silent = options?.silent ?? false;
|
||||||
|
if (!silent) setNodesLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getMonitorNodes();
|
const response = await getMonitorNodes();
|
||||||
|
|
||||||
@@ -40,11 +41,11 @@ export default function MonitorPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error(response.msg || "加载节点失败");
|
if (!silent) toast.error(response.msg || "加载节点失败");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("加载节点失败");
|
if (!silent) toast.error("加载节点失败");
|
||||||
} finally {
|
} finally {
|
||||||
setNodesLoading(false);
|
if (!silent) setNodesLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ export default function MonitorPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
void loadNodes();
|
void loadNodes({ silent: true });
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
@@ -86,7 +87,7 @@ export default function MonitorPage() {
|
|||||||
isLoading={nodesLoading}
|
isLoading={nodesLoading}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onPress={loadNodes}
|
onPress={() => loadNodes()}
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4 mr-1" />
|
<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 => {
|
const formatFlow = (bytes: number): string => {
|
||||||
if (!Number.isFinite(bytes) || bytes <= 0) {
|
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||||
@@ -719,17 +711,6 @@ export default function NodePage() {
|
|||||||
return "未知链路";
|
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 格式验证(仅用于判定地址族)
|
// IPv4/IPv6 格式验证(仅用于判定地址族)
|
||||||
const ipv4Regex =
|
const ipv4Regex =
|
||||||
@@ -2162,235 +2143,7 @@ export default function NodePage() {
|
|||||||
</div>
|
</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">
|
<div className="mt-auto space-y-3">
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Play,
|
Play,
|
||||||
Server,
|
Server,
|
||||||
Zap,
|
|
||||||
HardDrive,
|
|
||||||
Cpu,
|
|
||||||
Clock,
|
Clock,
|
||||||
Globe,
|
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@@ -173,91 +169,142 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode
|
|||||||
const isOnline = node.connectionStatus === "online";
|
const isOnline = node.connectionStatus === "online";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`border-t-4 transition-all hover:scale-[1.02] hover:shadow-lg cursor-pointer ${isOnline ? "border-t-success" : "border-t-danger"}`} onClick={onPress}>
|
<Card
|
||||||
<CardHeader className="pb-2 flex justify-between items-center">
|
className="group relative shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 h-full flex flex-col cursor-pointer"
|
||||||
<div className="flex items-center gap-2">
|
onClick={onPress}
|
||||||
<Server className={`w-5 h-5 ${isOnline ? "text-success" : "text-default-400"}`} />
|
>
|
||||||
<div className="font-semibold text-base truncate max-w-[150px]" title={node.name}>{node.name}</div>
|
<CardHeader className="pb-3">
|
||||||
</div>
|
<div className="flex justify-between items-start w-full gap-3">
|
||||||
<Chip size="sm" color={isOnline ? "success" : "danger"} variant="flat">
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
{isOnline ? "在线" : "离线"}
|
<div className="min-w-0 flex-1">
|
||||||
</Chip>
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
</CardHeader>
|
<h3 className="font-semibold text-foreground truncate text-sm leading-5">{node.name}</h3>
|
||||||
|
<span
|
||||||
<CardBody className="pt-0 space-y-4">
|
className={`h-2.5 w-2.5 rounded-full flex-shrink-0 ${isOnline ? "bg-emerald-500" : "bg-rose-500"}`}
|
||||||
<div className="grid grid-cols-2 gap-y-2 gap-x-4 text-xs mt-1">
|
title={isOnline ? "在线" : "离线"}
|
||||||
<div className="flex items-center justify-between">
|
/>
|
||||||
<div className="flex items-center gap-1 text-default-500">
|
</div>
|
||||||
<Clock className="w-3 h-3" /> 运行
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-default-500 flex-shrink-0">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
<span className="font-mono text-[11px]">{metric ? formatUptime(metric.uptime) : "-"}</span>
|
<span className="font-mono text-[11px]">{metric ? formatUptime(metric.uptime) : "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
</div>
|
||||||
<div className="flex items-center gap-1 text-default-500">
|
</CardHeader>
|
||||||
<Activity className="w-3 h-3" /> 负载
|
|
||||||
</div>
|
<CardBody className="pt-0 pb-3 flex-1 flex flex-col">
|
||||||
<span className="font-mono text-[11px]">{metric ? metric.load1.toFixed(2) : "-"}</span>
|
{/* IP & Version row */}
|
||||||
</div>
|
<div className="space-y-2 mb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex justify-between text-sm">
|
||||||
<div className="flex items-center gap-1 text-default-500">
|
<span className="text-default-600">版本</span>
|
||||||
<Globe className="w-3 h-3" /> 连接
|
<span className="text-xs font-mono">{metric ? "在线" : "-"}</span>
|
||||||
</div>
|
|
||||||
<span className="font-mono text-[11px]">{metric ? metric.tcpConns : "-"}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-1 text-default-500">
|
|
||||||
<Zap className="w-3 h-3" /> UDP
|
|
||||||
</div>
|
|
||||||
<span className="font-mono text-[11px]">{metric ? metric.udpConns : "-"}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
{/* System metrics */}
|
||||||
<div>
|
<div className="space-y-3 mb-3">
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="flex items-center gap-1 text-default-600"><Cpu className="w-3.5 h-3.5" /> CPU</div>
|
<div>
|
||||||
<span className="font-mono text-[11px]">{metric ? metric.cpuUsage.toFixed(1) : "0.0"}%</span>
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span>CPU</span>
|
||||||
|
<span className="font-mono">{isOnline && metric ? `${metric.cpuUsage.toFixed(1)}%` : "-"}</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
aria-label="CPU使用率"
|
||||||
|
color={getColorByUsage(metric?.cpuUsage)}
|
||||||
|
size="sm"
|
||||||
|
value={isOnline && metric ? metric.cpuUsage : 0}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={metric ? metric.cpuUsage : 0} color={getColorByUsage(metric?.cpuUsage)} className="h-1.5" />
|
<div>
|
||||||
</div>
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<div>
|
<span>内存</span>
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<span className="font-mono">{isOnline && metric ? `${metric.memoryUsage.toFixed(1)}%` : "-"}</span>
|
||||||
<div className="flex items-center gap-1 text-default-600"><HardDrive className="w-3.5 h-3.5" /> RAM</div>
|
</div>
|
||||||
<span className="font-mono text-[11px]">{metric ? metric.memoryUsage.toFixed(1) : "0.0"}%</span>
|
<Progress
|
||||||
|
aria-label="内存使用率"
|
||||||
|
color={getColorByUsage(metric?.memoryUsage)}
|
||||||
|
size="sm"
|
||||||
|
value={isOnline && metric ? metric.memoryUsage : 0}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={metric ? metric.memoryUsage : 0} color={getColorByUsage(metric?.memoryUsage)} className="h-1.5" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div className="flex justify-between text-xs mb-1">
|
|
||||||
<div className="flex items-center gap-1 text-default-600"><HardDrive className="w-3.5 h-3.5" /> Disk</div>
|
|
||||||
<span className="font-mono text-[11px]">{metric ? metric.diskUsage.toFixed(1) : "0.0"}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={metric ? metric.diskUsage : 0} color={getColorByUsage(metric?.diskUsage)} className="h-1.5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-3 border-t border-default-200/50 text-xs">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div className="flex flex-col gap-0.5 w-[50%] border-r border-default-200/50 pr-2">
|
<div className="text-center p-2 bg-default-50 dark:bg-default-100 rounded">
|
||||||
<div className="text-default-500 flex justify-between">
|
<div className="text-default-600 mb-0.5">上传</div>
|
||||||
<span>网络 ↓</span>
|
<div className="font-mono">{isOnline && metric ? formatBytesPerSecond(metric.netOutSpeed) : "-"}</div>
|
||||||
<span className="font-mono text-success">{metric ? formatBytesPerSecond(metric.netInSpeed) : "0 B/s"}</span>
|
</div>
|
||||||
</div>
|
<div className="text-center p-2 bg-default-50 dark:bg-default-100 rounded">
|
||||||
<div className="text-default-400 font-mono flex justify-between">
|
<div className="text-default-600 mb-0.5">下载</div>
|
||||||
<span>总计</span>
|
<div className="font-mono">{isOnline && metric ? formatBytesPerSecond(metric.netInSpeed) : "-"}</div>
|
||||||
<span className="text-[11px]">{metric ? formatBytes(metric.netInBytes) : "0 B"}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Traffic stats */}
|
||||||
<div className="flex flex-col gap-0.5 w-[50%] pl-2">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div className="text-default-500 flex justify-between">
|
<div className="text-center p-2 bg-primary-50 dark:bg-primary-100/20 rounded border border-primary-200 dark:border-primary-300/20">
|
||||||
<span>网络 ↑</span>
|
<div className="text-primary-600 dark:text-primary-400 mb-0.5">↑ 上行流量</div>
|
||||||
<span className="font-mono text-primary">{metric ? formatBytesPerSecond(metric.netOutSpeed) : "0 B/s"}</span>
|
<div className="font-mono text-primary-700 dark:text-primary-300">
|
||||||
</div>
|
{isOnline && metric ? formatBytes(metric.netOutBytes) : "-"}
|
||||||
<div className="text-default-400 font-mono flex justify-between">
|
</div>
|
||||||
<span>总计</span>
|
</div>
|
||||||
<span className="text-[11px]">{metric ? formatBytes(metric.netOutBytes) : "0 B"}</span>
|
<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>
|
<div className="text-success-600 dark:text-success-400 mb-0.5">↓ 下行流量</div>
|
||||||
</div>
|
<div className="font-mono text-success-700 dark:text-success-300">
|
||||||
|
{isOnline && metric ? formatBytes(metric.netInBytes) : "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Extended metrics */}
|
||||||
|
{isOnline && metric && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{metric.diskUsage > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span>磁盘</span>
|
||||||
|
<span className="font-mono">{metric.diskUsage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
aria-label="磁盘使用率"
|
||||||
|
color={getColorByUsage(metric.diskUsage)}
|
||||||
|
size="sm"
|
||||||
|
value={metric.diskUsage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(metric.load1 > 0 || metric.load5 > 0 || metric.load15 > 0) && (
|
||||||
|
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||||
|
<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">{metric.load1.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<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">{metric.load5.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<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">{metric.load15.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(metric.tcpConns > 0 || metric.udpConns > 0) && (
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<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">{metric.tcpConns}</div>
|
||||||
|
</div>
|
||||||
|
<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">{metric.udpConns}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -453,8 +500,9 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
}
|
}
|
||||||
}, [preferredNodeId, selectedNodeId]);
|
}, [preferredNodeId, selectedNodeId]);
|
||||||
|
|
||||||
const loadTunnels = useCallback(async () => {
|
const loadTunnels = useCallback(async (options?: { silent?: boolean }) => {
|
||||||
setTunnelsLoading(true);
|
const silent = options?.silent ?? false;
|
||||||
|
if (!silent) setTunnelsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getMonitorTunnels();
|
const response = await getMonitorTunnels();
|
||||||
|
|
||||||
@@ -473,12 +521,14 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTunnelsError(response.msg || "加载隧道列表失败");
|
setTunnelsError(response.msg || "加载隧道列表失败");
|
||||||
toast.error(response.msg || "加载隧道列表失败");
|
if (!silent) toast.error(response.msg || "加载隧道列表失败");
|
||||||
} catch {
|
} catch {
|
||||||
setTunnelsError("加载隧道列表失败");
|
if (!silent) {
|
||||||
toast.error("加载隧道列表失败");
|
setTunnelsError("加载隧道列表失败");
|
||||||
|
toast.error("加载隧道列表失败");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setTunnelsLoading(false);
|
if (!silent) setTunnelsLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -488,7 +538,7 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
void loadTunnels();
|
void loadTunnels({ silent: true });
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
@@ -501,8 +551,9 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
}, [selectedTunnelId, tunnels]);
|
}, [selectedTunnelId, tunnels]);
|
||||||
|
|
||||||
const loadTunnelMetrics = useCallback(
|
const loadTunnelMetrics = useCallback(
|
||||||
async (tunnelId: number) => {
|
async (tunnelId: number, options?: { silent?: boolean }) => {
|
||||||
setTunnelMetricsLoading(true);
|
const silent = options?.silent ?? false;
|
||||||
|
if (!silent) setTunnelMetricsLoading(true);
|
||||||
try {
|
try {
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const start = end - tunnelRangeMs;
|
const start = end - tunnelRangeMs;
|
||||||
@@ -529,12 +580,12 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
}
|
}
|
||||||
setTunnelMetricsTruncated(false);
|
setTunnelMetricsTruncated(false);
|
||||||
setTunnelMetricsError(response.msg || "加载隧道指标失败");
|
setTunnelMetricsError(response.msg || "加载隧道指标失败");
|
||||||
toast.error(response.msg || "加载隧道指标失败");
|
if (!silent) toast.error(response.msg || "加载隧道指标失败");
|
||||||
} catch {
|
} catch {
|
||||||
setTunnelMetricsTruncated(false);
|
setTunnelMetricsTruncated(false);
|
||||||
setTunnelMetricsError("加载隧道指标失败");
|
if (!silent) setTunnelMetricsError("加载隧道指标失败");
|
||||||
} finally {
|
} finally {
|
||||||
setTunnelMetricsLoading(false);
|
if (!silent) setTunnelMetricsLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tunnelRangeMs],
|
[tunnelRangeMs],
|
||||||
@@ -549,15 +600,16 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedTunnelId) return;
|
if (!selectedTunnelId) return;
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
void loadTunnelMetrics(selectedTunnelId);
|
void loadTunnelMetrics(selectedTunnelId, { silent: true });
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [selectedTunnelId, loadTunnelMetrics]);
|
}, [selectedTunnelId, loadTunnelMetrics]);
|
||||||
|
|
||||||
const loadMetrics = useCallback(
|
const loadMetrics = useCallback(
|
||||||
async (nodeId: number) => {
|
async (nodeId: number, options?: { silent?: boolean }) => {
|
||||||
setMetricsLoading(true);
|
const silent = options?.silent ?? false;
|
||||||
|
if (!silent) setMetricsLoading(true);
|
||||||
try {
|
try {
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const start = end - metricsRangeMs;
|
const start = end - metricsRangeMs;
|
||||||
@@ -584,19 +636,20 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
}
|
}
|
||||||
setMetricsTruncated(false);
|
setMetricsTruncated(false);
|
||||||
setMetricsError(response.msg || "加载指标失败");
|
setMetricsError(response.msg || "加载指标失败");
|
||||||
toast.error(response.msg || "加载指标失败");
|
if (!silent) toast.error(response.msg || "加载指标失败");
|
||||||
} catch {
|
} catch {
|
||||||
setMetricsTruncated(false);
|
setMetricsTruncated(false);
|
||||||
setMetricsError("加载指标失败");
|
if (!silent) setMetricsError("加载指标失败");
|
||||||
} finally {
|
} finally {
|
||||||
setMetricsLoading(false);
|
if (!silent) setMetricsLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[metricsRangeMs],
|
[metricsRangeMs],
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadServiceMonitors = useCallback(async () => {
|
const loadServiceMonitors = useCallback(async (options?: { silent?: boolean }) => {
|
||||||
setMonitorsLoading(true);
|
const silent = options?.silent ?? false;
|
||||||
|
if (!silent) setMonitorsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getServiceMonitorList();
|
const response = await getServiceMonitorList();
|
||||||
|
|
||||||
@@ -614,11 +667,11 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMonitorsError(response.msg || "加载服务监控失败");
|
setMonitorsError(response.msg || "加载服务监控失败");
|
||||||
toast.error(response.msg || "加载服务监控失败");
|
if (!silent) toast.error(response.msg || "加载服务监控失败");
|
||||||
} catch {
|
} catch {
|
||||||
setMonitorsError("加载服务监控失败");
|
if (!silent) setMonitorsError("加载服务监控失败");
|
||||||
} finally {
|
} finally {
|
||||||
setMonitorsLoading(false);
|
if (!silent) setMonitorsLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -731,7 +784,7 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
void loadServiceMonitors();
|
void loadServiceMonitors({ silent: true });
|
||||||
void loadLatestMonitorResults();
|
void loadLatestMonitorResults();
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
@@ -747,7 +800,7 @@ export function MonitorView({ nodeMap }: MonitorViewProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedNodeId) return;
|
if (!selectedNodeId) return;
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
void loadMetrics(selectedNodeId);
|
void loadMetrics(selectedNodeId, { silent: true });
|
||||||
}, 15_000);
|
}, 15_000);
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
|
|||||||
Reference in New Issue
Block a user