Files
SenMeshNet/components/monitoring/MonitoringDetailPanel.tsx
zhaolei 2f9f8da48e
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
2026-10-15 13:53:04 +08:00

468 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
// 告警阈值(从主进程告警规则动态派生)
thresholds?: {
cpuCritical: number;
cpuWarning: number;
memCritical: number;
memWarning: number;
diskCritical: number;
diskWarning: number;
};
}
// ============ 辅助函数 ============
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: '在线' },
normal: { 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,
thresholds = { cpuCritical: 90, cpuWarning: 80, memCritical: 85, memWarning: 80, diskCritical: 90, diskWarning: 80 },
}: 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 > thresholds.cpuCritical ? 'text-red-400' : d.cpu > thresholds.cpuWarning ? '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 > thresholds.memCritical ? 'text-red-400' : d.memory > thresholds.memWarning ? '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 > thresholds.cpuCritical ? 'text-red-400' : deviceDetail.cpu && deviceDetail.cpu > thresholds.cpuWarning ? 'text-amber-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 > thresholds.cpuCritical ? '#f87171'
: deviceDetail.cpu && deviceDetail.cpu > thresholds.cpuWarning ? '#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 > thresholds.memCritical ? 'text-red-400' : deviceDetail.memory && deviceDetail.memory > thresholds.memWarning ? 'text-amber-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 > thresholds.memCritical ? '#f87171'
: deviceDetail.memory && deviceDetail.memory > thresholds.memWarning ? '#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 > thresholds.diskCritical ? 'text-red-400' : d.usage > thresholds.diskWarning ? '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 > thresholds.diskCritical ? '#f87171' : d.usage > thresholds.diskWarning ? '#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>
</>
);
}