feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括: - 巡检数据存储与调度系统 - 华为/H3C交换机及Linux服务器采集器 - 设备扩展配置与告警规则管理 - 主机详情页巡检配置入口 - 产品名称自定义设置功能 - 网络设备SSH exec通道兼容处理
This commit is contained in:
456
components/monitoring/MonitoringDetailPanel.tsx
Normal file
456
components/monitoring/MonitoringDetailPanel.tsx
Normal file
@@ -0,0 +1,456 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
X,
|
||||
Server,
|
||||
Cpu,
|
||||
HardDrive,
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Clock,
|
||||
Loader2,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
|
||||
// ============ 类型定义 ============
|
||||
|
||||
export type PanelType =
|
||||
| 'deviceList' // 设备列表(按状态筛选)
|
||||
| 'alertList' // 告警列表(按级别筛选)
|
||||
| 'deviceDetail' // 单设备详情
|
||||
| 'taskList'; // 巡检任务列表(按状态筛选)
|
||||
|
||||
export interface DetailPanelProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
panelType: PanelType;
|
||||
panelTitle: string;
|
||||
// 设备列表
|
||||
deviceList?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
ip?: string;
|
||||
type: string;
|
||||
status: 'online' | 'offline' | 'warning' | 'critical';
|
||||
cpu?: number;
|
||||
memory?: number;
|
||||
lastUpdate?: number;
|
||||
alertCount?: number;
|
||||
}>;
|
||||
// 告警列表
|
||||
alertList?: Array<{
|
||||
id: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
level: 'critical' | 'warning' | 'info';
|
||||
message: string;
|
||||
time: string;
|
||||
metric?: string;
|
||||
metricValue?: string;
|
||||
threshold?: string;
|
||||
}>;
|
||||
// 设备详情
|
||||
deviceDetail?: {
|
||||
id: string;
|
||||
name: string;
|
||||
ip?: string;
|
||||
type: string;
|
||||
status: 'online' | 'offline' | 'warning' | 'critical';
|
||||
cpu?: number;
|
||||
memory?: number;
|
||||
disk?: Array<{ mount: string; usage: number }>;
|
||||
lastUpdate?: number;
|
||||
alerts?: Array<{
|
||||
id: string;
|
||||
level: string;
|
||||
message: string;
|
||||
time: string;
|
||||
}>;
|
||||
};
|
||||
// 任务列表
|
||||
taskList?: Array<{
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
ip: string;
|
||||
status: 'running' | 'success' | 'failed' | 'pending';
|
||||
lastScanAt?: number;
|
||||
durationMs?: number;
|
||||
errorMessage?: string;
|
||||
interval?: number;
|
||||
}>;
|
||||
// 点击设备回调
|
||||
onDeviceClick?: (deviceId: string) => void;
|
||||
// 点击告警回调
|
||||
onAlertClick?: (alertId: string) => void;
|
||||
}
|
||||
|
||||
// ============ 辅助函数 ============
|
||||
|
||||
const statusColors: Record<string, { bg: string; text: string; dot: string; label: string }> = {
|
||||
online: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-500', label: '在线' },
|
||||
offline: { bg: 'bg-slate-500/15', text: 'text-slate-400', dot: 'bg-slate-500', label: '离线' },
|
||||
warning: { bg: 'bg-amber-500/15', text: 'text-amber-400', dot: 'bg-amber-500', label: '警告' },
|
||||
critical: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '严重' },
|
||||
running: { bg: 'bg-sky-500/15', text: 'text-sky-400', dot: 'bg-sky-500', label: '执行中' },
|
||||
success: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-500', label: '成功' },
|
||||
failed: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '失败' },
|
||||
pending: { bg: 'bg-slate-500/15', text: 'text-slate-400', dot: 'bg-slate-500', label: '待执行' },
|
||||
};
|
||||
|
||||
const levelColors: Record<string, { bg: string; text: string; dot: string; label: string }> = {
|
||||
critical: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-500', label: '严重' },
|
||||
major: { bg: 'bg-orange-500/15', text: 'text-orange-400', dot: 'bg-orange-500', label: '重要' },
|
||||
warning: { bg: 'bg-amber-500/15', text: 'text-amber-400', dot: 'bg-amber-500', label: '警告' },
|
||||
minor: { bg: 'bg-yellow-500/15', text: 'text-yellow-400', dot: 'bg-yellow-500', label: '次要' },
|
||||
info: { bg: 'bg-sky-500/15', text: 'text-sky-400', dot: 'bg-sky-500', label: '提示' },
|
||||
};
|
||||
|
||||
function formatRelativeTime(ts?: number): string {
|
||||
if (!ts) return '-';
|
||||
const diff = Date.now() - ts;
|
||||
if (diff < 60000) return '刚刚';
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
|
||||
return `${Math.floor(diff / 86400000)} 天前`;
|
||||
}
|
||||
|
||||
// ============ 组件 ============
|
||||
|
||||
export default function MonitoringDetailPanel({
|
||||
open,
|
||||
onClose,
|
||||
panelType,
|
||||
panelTitle,
|
||||
deviceList = [],
|
||||
alertList = [],
|
||||
deviceDetail,
|
||||
taskList = [],
|
||||
onDeviceClick,
|
||||
}: DetailPanelProps) {
|
||||
|
||||
// ============ 设备列表渲染 ============
|
||||
const renderDeviceList = () => (
|
||||
<div className="space-y-2">
|
||||
{deviceList.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground text-sm">暂无数据</div>
|
||||
)}
|
||||
{deviceList.map((d) => {
|
||||
const sc = statusColors[d.status] || statusColors.online;
|
||||
return (
|
||||
<div
|
||||
key={d.id}
|
||||
className="p-3 rounded-lg border cursor-pointer transition-all hover:border-cyan-500/40 hover:bg-cyan-500/5"
|
||||
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
|
||||
onClick={() => onDeviceClick?.(d.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className={`w-2 h-2 rounded-full ${sc.dot} flex-shrink-0`} />
|
||||
<span className="text-sm font-medium text-foreground truncate">{d.name}</span>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
{d.ip && <span>{d.ip}</span>}
|
||||
<span>{d.type}</span>
|
||||
<span className={`${sc.text}`}>{sc.label}</span>
|
||||
</div>
|
||||
{(d.cpu !== undefined || d.memory !== undefined) && (
|
||||
<div className="flex items-center gap-4 mt-2 text-xs">
|
||||
{d.cpu !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Cpu className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">CPU</span>
|
||||
<span className={d.cpu > 80 ? 'text-red-400' : d.cpu > 60 ? 'text-amber-400' : 'text-emerald-400'}>
|
||||
{d.cpu}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{d.memory !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<HardDrive className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">内存</span>
|
||||
<span className={d.memory > 85 ? 'text-red-400' : d.memory > 70 ? 'text-amber-400' : 'text-emerald-400'}>
|
||||
{d.memory}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 告警列表渲染 ============
|
||||
const renderAlertList = () => (
|
||||
<div className="space-y-2">
|
||||
{alertList.length === 0 && (
|
||||
<div className="text-center py-8 text-emerald-400 text-sm">暂无告警</div>
|
||||
)}
|
||||
{alertList.map((a) => {
|
||||
const lc = levelColors[a.level] || levelColors.info;
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
className="p-3 rounded-lg border transition-all"
|
||||
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className={`w-2 h-2 rounded-full ${lc.dot} flex-shrink-0 mt-0.5`} />
|
||||
<span className={`text-xs font-medium ${lc.text} flex-shrink-0`}>{lc.label}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground flex-shrink-0">{a.time}</span>
|
||||
</div>
|
||||
<div className="text-sm text-foreground mb-1">{a.message}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
设备:<span className="text-cyan-400">{a.deviceName}</span>
|
||||
{a.metric && a.metricValue && (
|
||||
<span className="ml-3">
|
||||
{a.metric}: {a.metricValue}
|
||||
{a.threshold && <span className="text-muted-foreground"> (阈值 {a.threshold})</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 设备详情渲染 ============
|
||||
const renderDeviceDetail = () => {
|
||||
if (!deviceDetail) return null;
|
||||
const sc = statusColors[deviceDetail.status] || statusColors.online;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 基本信息 */}
|
||||
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Server className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-sm font-medium">基本信息</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">设备名称</div>
|
||||
<div className="text-foreground font-medium">{deviceDetail.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">设备类型</div>
|
||||
<div className="text-foreground">{deviceDetail.type}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">IP 地址</div>
|
||||
<div className="text-foreground font-mono">{deviceDetail.ip || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-0.5">运行状态</div>
|
||||
<div className={`${sc.text} font-medium`}>{sc.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 资源使用 */}
|
||||
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Activity className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-sm font-medium">资源使用</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{/* CPU */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span className="text-muted-foreground">CPU 使用率</span>
|
||||
<span className={deviceDetail.cpu && deviceDetail.cpu > 80 ? 'text-red-400' : 'text-foreground'}>
|
||||
{deviceDetail.cpu ?? '-'}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${deviceDetail.cpu ?? 0}%`,
|
||||
background: deviceDetail.cpu && deviceDetail.cpu > 80 ? '#f87171'
|
||||
: deviceDetail.cpu && deviceDetail.cpu > 60 ? '#fbbf24' : '#34d399',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 内存 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span className="text-muted-foreground">内存使用率</span>
|
||||
<span className={deviceDetail.memory && deviceDetail.memory > 85 ? 'text-red-400' : 'text-foreground'}>
|
||||
{deviceDetail.memory ?? '-'}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${deviceDetail.memory ?? 0}%`,
|
||||
background: deviceDetail.memory && deviceDetail.memory > 85 ? '#f87171'
|
||||
: deviceDetail.memory && deviceDetail.memory > 70 ? '#fbbf24' : '#34d399',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 磁盘 */}
|
||||
{deviceDetail.disk && deviceDetail.disk.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-muted-foreground">磁盘使用率</div>
|
||||
{deviceDetail.disk.map((d, i) => (
|
||||
<div key={i}>
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span className="text-muted-foreground font-mono">{d.mount}</span>
|
||||
<span className={d.usage > 90 ? 'text-red-400' : d.usage > 80 ? 'text-amber-400' : 'text-foreground'}>
|
||||
{d.usage}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${d.usage}%`,
|
||||
background: d.usage > 90 ? '#f87171' : d.usage > 80 ? '#fbbf24' : '#34d399',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 更新时间 */}
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1.5">
|
||||
<Clock className="w-3 h-3" />
|
||||
最后更新:{formatRelativeTime(deviceDetail.lastUpdate)}
|
||||
</div>
|
||||
|
||||
{/* 告警历史 */}
|
||||
{deviceDetail.alerts && deviceDetail.alerts.length > 0 && (
|
||||
<div className="p-3 rounded-lg border" style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-sm font-medium">告警记录</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{deviceDetail.alerts.map((a) => {
|
||||
const lc = levelColors[a.level] || levelColors.info;
|
||||
return (
|
||||
<div key={a.id} className="flex items-start gap-2 text-xs">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${lc.dot} flex-shrink-0 mt-1`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-foreground">{a.message}</div>
|
||||
<div className="text-muted-foreground mt-0.5">{a.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============ 任务列表渲染 ============
|
||||
const renderTaskList = () => (
|
||||
<div className="space-y-2">
|
||||
{taskList.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground text-sm">暂无数据</div>
|
||||
)}
|
||||
{taskList.map((t) => {
|
||||
const sc = statusColors[t.status] || statusColors.pending;
|
||||
const durationStr = t.durationMs ? `${(t.durationMs / 1000).toFixed(1)}s` : null;
|
||||
const intervalStr = t.interval ? `${t.interval}秒` : null;
|
||||
return (
|
||||
<div
|
||||
key={t.deviceId}
|
||||
className="p-3 rounded-lg border transition-all"
|
||||
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{t.status === 'running' && <Loader2 className="w-3.5 h-3.5 text-sky-400 animate-spin flex-shrink-0" />}
|
||||
{t.status === 'success' && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-400 flex-shrink-0" />}
|
||||
{t.status === 'failed' && <XCircle className="w-3.5 h-3.5 text-red-400 flex-shrink-0" />}
|
||||
{t.status === 'pending' && <Clock className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />}
|
||||
<span className="text-sm font-medium text-foreground truncate">{t.deviceName}</span>
|
||||
</div>
|
||||
<span className={`text-xs font-medium ${sc.text} flex-shrink-0`}>{sc.label}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono mb-1">{t.ip}</div>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground flex-wrap">
|
||||
{t.lastScanAt && <span>上次 {formatRelativeTime(t.lastScanAt)}</span>}
|
||||
{durationStr && <span>耗时 {durationStr}</span>}
|
||||
{intervalStr && <span>间隔 {intervalStr}</span>}
|
||||
</div>
|
||||
{t.errorMessage && (
|
||||
<div className="mt-2 text-xs text-red-400 bg-red-500/10 p-2 rounded">
|
||||
{t.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ============ 主渲染 ============
|
||||
return (
|
||||
<>
|
||||
{/* 遮罩 */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-black/50 z-40 transition-opacity ${
|
||||
open ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* 面板 */}
|
||||
<div
|
||||
className={`absolute right-0 top-0 bottom-0 z-50 transition-transform duration-300 ease-out flex flex-col ${
|
||||
open ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
style={{
|
||||
width: '420px',
|
||||
background: '#0f172a',
|
||||
borderLeft: '1px solid rgba(148,163,184,0.2)',
|
||||
}}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-3 flex-shrink-0"
|
||||
style={{ borderBottom: '1px solid rgba(148,163,184,0.15)' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-1 h-5 rounded-full"
|
||||
style={{ background: 'linear-gradient(180deg, #06b6d4, #3b82f6)' }}
|
||||
/>
|
||||
<span className="text-sm font-medium text-foreground">{panelTitle}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded hover:bg-slate-700/50 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容区 */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
{panelType === 'deviceList' && renderDeviceList()}
|
||||
{panelType === 'alertList' && renderAlertList()}
|
||||
{panelType === 'deviceDetail' && renderDeviceDetail()}
|
||||
{panelType === 'taskList' && renderTaskList()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user