1.18升级
This commit is contained in:
parent
38b6e873c8
commit
eebd3240b3
@ -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<ServerMonitorDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
server,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [monitorData, setMonitorData] = useState<ServerMonitorResponse | null>(null);
|
||||
const [selectedTimeRange, setSelectedTimeRange] = useState<TimeRange>('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')}<br/>
|
||||
CPU: ${param.value}%<br/>
|
||||
状态: ${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')}<br/>
|
||||
内存: ${param.value}%<br/>
|
||||
已用: ${dataItem.usedGB}GB<br/>
|
||||
状态: ${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')}<br/>
|
||||
接收: ${dataItem.rxMBps.toFixed(2)} MB/s<br/>
|
||||
发送: ${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 (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
暂无磁盘数据
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const disk = monitorData.metrics.disk;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">磁盘使用情况</h3>
|
||||
<Badge variant="outline">
|
||||
最大使用率: {disk.maxUsagePercent.toFixed(2)}% ({disk.maxUsagePartition})
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{disk.partitions.map((partition, index) => (
|
||||
<Card key={index}>
|
||||
<CardContent className="pt-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-mono font-medium">{partition.mountPoint}</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant={partition.usagePercent > 80 ? 'destructive' : 'outline'}
|
||||
>
|
||||
{partition.usagePercent.toFixed(2)}%
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{partition.fileSystem}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
已用 {partition.usedSizeGB}GB / 总计 {partition.totalSizeGB}GB
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-secondary rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${
|
||||
partition.usagePercent > 80
|
||||
? 'bg-destructive'
|
||||
: partition.usagePercent > 60
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-primary'
|
||||
}`}
|
||||
style={{ width: `${partition.usagePercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 统计信息卡片
|
||||
const renderStatistics = () => {
|
||||
if (!monitorData) return null;
|
||||
|
||||
const stats = [];
|
||||
|
||||
if (monitorData.statistics.cpu) {
|
||||
stats.push({
|
||||
icon: <Activity className="h-5 w-5" />,
|
||||
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: <TrendingUp className="h-5 w-5" />,
|
||||
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: <NetworkIcon className="h-5 w-5" />,
|
||||
label: '网络',
|
||||
avg: `接收 ${totalRxMB}MB`,
|
||||
max: `发送 ${totalTxMB}MB`,
|
||||
min: '',
|
||||
color: 'text-purple-600',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index}>
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`${stat.color}`}>{stat.icon}</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-muted-foreground">{stat.label}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1 space-y-0.5">
|
||||
<div>平均: {stat.avg}</div>
|
||||
<div>最大: {stat.max}</div>
|
||||
{stat.min && <div>最小: {stat.min}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
服务器监控 - {server?.serverName} ({server?.hostIp})
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody className="flex-1 overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
{/* 时间范围选择器 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{Object.entries(TimeRangeLabels).map(([key, value]) => (
|
||||
<Button
|
||||
key={key}
|
||||
variant={selectedTimeRange === key ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSelectedTimeRange(key as TimeRange)}
|
||||
>
|
||||
{value.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedTimeRange === 'LAST_1_HOUR' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||
>
|
||||
{autoRefresh ? '停止自动刷新' : '开启自动刷新'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadMonitorData}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : '刷新'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && !monitorData ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : monitorData ? (
|
||||
<>
|
||||
{/* 统计信息 */}
|
||||
{renderStatistics()}
|
||||
|
||||
{/* 图表标签页 */}
|
||||
<Tabs defaultValue="cpu" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="cpu">CPU</TabsTrigger>
|
||||
<TabsTrigger value="memory">内存</TabsTrigger>
|
||||
<TabsTrigger value="network">网络</TabsTrigger>
|
||||
<TabsTrigger value="disk">磁盘</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="cpu" className="space-y-4">
|
||||
{monitorData.metrics.cpu && monitorData.metrics.cpu.length > 0 ? (
|
||||
<ReactECharts
|
||||
option={getCpuChartOption()}
|
||||
style={{ height: '400px', width: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
暂无CPU数据
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="memory" className="space-y-4">
|
||||
{monitorData.metrics.memory && monitorData.metrics.memory.length > 0 ? (
|
||||
<ReactECharts
|
||||
option={getMemoryChartOption()}
|
||||
style={{ height: '400px', width: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
暂无内存数据
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="network" className="space-y-4">
|
||||
{monitorData.metrics.network && monitorData.metrics.network.length > 0 ? (
|
||||
<ReactECharts
|
||||
option={getNetworkChartOption()}
|
||||
style={{ height: '400px', width: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
暂无网络数据
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="disk" className="space-y-4">
|
||||
{renderDiskInfo()}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||
暂无数据
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user