From eebd3240b34674a460ee09b09f4d23cb72f94445 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 11 Dec 2025 13:44:26 +0800 Subject: [PATCH] =?UTF-8?q?1.18=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../List/components/ServerMonitorDialog.tsx | 570 ++++++++++++++++++ 1 file changed, 570 insertions(+) create mode 100644 frontend/src/pages/Resource/Server/List/components/ServerMonitorDialog.tsx diff --git a/frontend/src/pages/Resource/Server/List/components/ServerMonitorDialog.tsx b/frontend/src/pages/Resource/Server/List/components/ServerMonitorDialog.tsx new file mode 100644 index 00000000..f853afc3 --- /dev/null +++ b/frontend/src/pages/Resource/Server/List/components/ServerMonitorDialog.tsx @@ -0,0 +1,570 @@ +import React, { useState, useEffect } from 'react'; +import ReactECharts from 'echarts-for-react'; +import type { EChartsOption } from 'echarts'; +import { Loader2, TrendingUp, Activity, HardDrive, Network as NetworkIcon } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogBody, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { useToast } from '@/components/ui/use-toast'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Card, CardContent } from '@/components/ui/card'; +import type { ServerResponse, ServerMonitorResponse, TimeRange, MetricType } from '../types'; +import { TimeRangeLabels, MetricTypeLabels } from '../types'; +import { getServerMonitorMetrics } from '../service'; +import dayjs from 'dayjs'; + +interface ServerMonitorDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + server: ServerResponse | null; +} + +export const ServerMonitorDialog: React.FC = ({ + open, + onOpenChange, + server, +}) => { + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [monitorData, setMonitorData] = useState(null); + const [selectedTimeRange, setSelectedTimeRange] = useState('LAST_1_HOUR' as TimeRange); + const [autoRefresh, setAutoRefresh] = useState(true); + + // 加载监控数据 + const loadMonitorData = async () => { + if (!server) return; + + try { + setLoading(true); + const response = await getServerMonitorMetrics(server.id, selectedTimeRange); + setMonitorData(response); + } catch (error: any) { + console.error('加载监控数据失败:', error); + } finally { + setLoading(false); + } + }; + + // 初始加载和时间范围变化时加载数据 + useEffect(() => { + if (open && server) { + loadMonitorData(); + } + }, [open, server, selectedTimeRange]); + + // 自动刷新(仅最近1小时) + useEffect(() => { + if (!open || !autoRefresh || selectedTimeRange !== 'LAST_1_HOUR') { + return; + } + + const timer = setInterval(() => { + loadMonitorData(); + }, 30000); // 30秒刷新一次 + + return () => clearInterval(timer); + }, [open, autoRefresh, selectedTimeRange]); + + // CPU 图表配置 + const getCpuChartOption = (): EChartsOption => { + if (!monitorData?.metrics.cpu || monitorData.metrics.cpu.length === 0) { + return {}; + } + + const cpuData = monitorData.metrics.cpu; + const statistics = monitorData.statistics.cpu; + + return { + title: { + text: 'CPU 使用率', + left: 'center', + textStyle: { fontSize: 14, fontWeight: 'bold' }, + }, + tooltip: { + trigger: 'axis', + formatter: (params: any) => { + const param = params[0]; + const dataItem = cpuData[param.dataIndex]; + return `${dayjs(param.name).format('MM-DD HH:mm')}
+ CPU: ${param.value}%
+ 状态: ${dataItem.status === 'SUCCESS' ? '正常' : '失败'}`; + }, + }, + grid: { + left: '3%', + right: '4%', + bottom: '10%', + top: '20%', + containLabel: true, + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: cpuData.map(item => item.time), + axisLabel: { + formatter: (value: string) => dayjs(value).format('HH:mm'), + }, + }, + yAxis: { + type: 'value', + name: 'CPU (%)', + max: 100, + axisLabel: { + formatter: '{value}%', + }, + }, + series: [ + { + name: 'CPU使用率', + type: 'line', + smooth: true, + data: cpuData.map(item => item.value), + lineStyle: { + color: MetricTypeLabels.CPU.color, + width: 2, + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: `${MetricTypeLabels.CPU.color}40` }, + { offset: 1, color: `${MetricTypeLabels.CPU.color}10` }, + ], + }, + }, + markLine: statistics + ? { + data: [ + { yAxis: statistics.avg, name: '平均值', lineStyle: { type: 'dashed', color: '#666' } }, + ], + label: { + formatter: '平均: {c}%', + }, + } + : undefined, + }, + ], + }; + }; + + // 内存图表配置 + const getMemoryChartOption = (): EChartsOption => { + if (!monitorData?.metrics.memory || monitorData.metrics.memory.length === 0) { + return {}; + } + + const memoryData = monitorData.metrics.memory; + const statistics = monitorData.statistics.memory; + + return { + title: { + text: '内存使用率', + left: 'center', + textStyle: { fontSize: 14, fontWeight: 'bold' }, + }, + tooltip: { + trigger: 'axis', + formatter: (params: any) => { + const param = params[0]; + const dataItem = memoryData[param.dataIndex]; + return `${dayjs(param.name).format('MM-DD HH:mm')}
+ 内存: ${param.value}%
+ 已用: ${dataItem.usedGB}GB
+ 状态: ${dataItem.status === 'SUCCESS' ? '正常' : '失败'}`; + }, + }, + grid: { + left: '3%', + right: '4%', + bottom: '10%', + top: '20%', + containLabel: true, + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: memoryData.map(item => item.time), + axisLabel: { + formatter: (value: string) => dayjs(value).format('HH:mm'), + }, + }, + yAxis: { + type: 'value', + name: '内存 (%)', + max: 100, + axisLabel: { + formatter: '{value}%', + }, + }, + series: [ + { + name: '内存使用率', + type: 'line', + smooth: true, + data: memoryData.map(item => item.usagePercent), + lineStyle: { + color: MetricTypeLabels.MEMORY.color, + width: 2, + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: `${MetricTypeLabels.MEMORY.color}40` }, + { offset: 1, color: `${MetricTypeLabels.MEMORY.color}10` }, + ], + }, + }, + markLine: statistics + ? { + data: [ + { yAxis: statistics.avgPercent, name: '平均值', lineStyle: { type: 'dashed', color: '#666' } }, + ], + label: { + formatter: '平均: {c}%', + }, + } + : undefined, + }, + ], + }; + }; + + // 网络流量图表配置 + const getNetworkChartOption = (): EChartsOption => { + if (!monitorData?.metrics.network || monitorData.metrics.network.length === 0) { + return {}; + } + + const networkData = monitorData.metrics.network; + + return { + title: { + text: '网络流量', + left: 'center', + textStyle: { fontSize: 14, fontWeight: 'bold' }, + }, + tooltip: { + trigger: 'axis', + formatter: (params: any) => { + const dataItem = networkData[params[0].dataIndex]; + return `${dayjs(params[0].name).format('MM-DD HH:mm')}
+ 接收: ${dataItem.rxMBps.toFixed(2)} MB/s
+ 发送: ${dataItem.txMBps.toFixed(2)} MB/s`; + }, + }, + legend: { + data: ['接收', '发送'], + top: '8%', + }, + grid: { + left: '3%', + right: '4%', + bottom: '10%', + top: '20%', + containLabel: true, + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: networkData.map(item => item.time), + axisLabel: { + formatter: (value: string) => dayjs(value).format('HH:mm'), + }, + }, + yAxis: { + type: 'value', + name: '流量 (MB/s)', + axisLabel: { + formatter: '{value}', + }, + }, + series: [ + { + name: '接收', + type: 'line', + smooth: true, + data: networkData.map(item => item.rxMBps), + lineStyle: { + color: '#3b82f6', + width: 2, + }, + }, + { + name: '发送', + type: 'line', + smooth: true, + data: networkData.map(item => item.txMBps), + lineStyle: { + color: '#8b5cf6', + width: 2, + }, + }, + ], + }; + }; + + // 磁盘使用情况卡片 + const renderDiskInfo = () => { + if (!monitorData?.metrics.disk) { + return ( +
+ 暂无磁盘数据 +
+ ); + } + + const disk = monitorData.metrics.disk; + + return ( +
+
+

磁盘使用情况

+ + 最大使用率: {disk.maxUsagePercent.toFixed(2)}% ({disk.maxUsagePartition}) + +
+
+ {disk.partitions.map((partition, index) => ( + + +
+
+
+ + {partition.mountPoint} +
+ 80 ? 'destructive' : 'outline'} + > + {partition.usagePercent.toFixed(2)}% + +
+
+ {partition.fileSystem} +
+
+ + 已用 {partition.usedSizeGB}GB / 总计 {partition.totalSizeGB}GB + +
+
+
80 + ? 'bg-destructive' + : partition.usagePercent > 60 + ? 'bg-yellow-500' + : 'bg-primary' + }`} + style={{ width: `${partition.usagePercent}%` }} + /> +
+
+ + + ))} +
+
+ ); + }; + + // 统计信息卡片 + const renderStatistics = () => { + if (!monitorData) return null; + + const stats = []; + + if (monitorData.statistics.cpu) { + stats.push({ + icon: , + label: 'CPU', + avg: `${monitorData.statistics.cpu.avg.toFixed(2)}%`, + max: `${monitorData.statistics.cpu.max.toFixed(2)}%`, + min: `${monitorData.statistics.cpu.min.toFixed(2)}%`, + color: 'text-blue-600', + }); + } + + if (monitorData.statistics.memory) { + stats.push({ + icon: , + label: '内存', + avg: `${monitorData.statistics.memory.avgPercent.toFixed(2)}%`, + max: `${monitorData.statistics.memory.maxPercent.toFixed(2)}%`, + min: `${monitorData.statistics.memory.minPercent.toFixed(2)}%`, + color: 'text-green-600', + }); + } + + if (monitorData.statistics.network) { + const network = monitorData.statistics.network; + const totalRxMB = (network.totalRxBytes / 1024 / 1024).toFixed(2); + const totalTxMB = (network.totalTxBytes / 1024 / 1024).toFixed(2); + + stats.push({ + icon: , + label: '网络', + avg: `接收 ${totalRxMB}MB`, + max: `发送 ${totalTxMB}MB`, + min: '', + color: 'text-purple-600', + }); + } + + return ( +
+ {stats.map((stat, index) => ( + + +
+
{stat.icon}
+
+
{stat.label}
+
+
平均: {stat.avg}
+
最大: {stat.max}
+ {stat.min &&
最小: {stat.min}
} +
+
+
+
+
+ ))} +
+ ); + }; + + return ( + + + + + 服务器监控 - {server?.serverName} ({server?.hostIp}) + + + + +
+ {/* 时间范围选择器 */} +
+
+ {Object.entries(TimeRangeLabels).map(([key, value]) => ( + + ))} +
+
+ {selectedTimeRange === 'LAST_1_HOUR' && ( + + )} + +
+
+ + {loading && !monitorData ? ( +
+ +
+ ) : monitorData ? ( + <> + {/* 统计信息 */} + {renderStatistics()} + + {/* 图表标签页 */} + + + CPU + 内存 + 网络 + 磁盘 + + + + {monitorData.metrics.cpu && monitorData.metrics.cpu.length > 0 ? ( + + ) : ( +
+ 暂无CPU数据 +
+ )} +
+ + + {monitorData.metrics.memory && monitorData.metrics.memory.length > 0 ? ( + + ) : ( +
+ 暂无内存数据 +
+ )} +
+ + + {monitorData.metrics.network && monitorData.metrics.network.length > 0 ? ( + + ) : ( +
+ 暂无网络数据 +
+ )} +
+ + + {renderDiskInfo()} + +
+ + ) : ( +
+ 暂无数据 +
+ )} +
+
+
+
+ ); +};