feat: monitor page redesign and node card cleanup (#340)

This commit is contained in:
sagit
2026-03-18 18:57:23 +08:00
committed by GitHub
parent 7134253b2c
commit 555039e028
4 changed files with 172 additions and 358 deletions
@@ -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
+8 -7
View File
@@ -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" />
刷新节点 刷新节点
-247
View File
@@ -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">
{/* 操作按钮 */} {/* 操作按钮 */}
+157 -104
View File
@@ -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);