Files
SenMeshNet/components/monitoring/MonitoringDetailPanel.tsx
zhaolei a5744e720b
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
feat(plan-management): implement alert-remediation-plan system v1.3
- 8 new store modules + matcher + initialResponse + aggregation + remediation + AlertStateMonitor + planGenerator

- alert chain auto-integrated after runInspection: freeze snapshot + generate response + real-time agg + auto-start monitor

- AlertAIPanel component + MonitoringDetailPanel expandable alert list

- PlanManagementPage: full page with list/edit/AI generate/templates/commands
2026-10-30 16:55:55 +08:00

526 lines
22 KiB
TypeScript
Raw Permalink 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,
ChevronDown,
ChevronUp,
} from 'lucide-react';
import AlertAIPanel from './AlertAIPanel';
import type { DevicePlan, InitialResponse, RemediationRun } from '../../infrastructure/inspection/inspectionApi';
// ============ 类型定义 ============
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;
// v1.3 预案管理扩展字段
matched_plan_snapshot?: DevicePlan | null;
initial_response?: InitialResponse | null;
remediation?: RemediationRun | null;
escalation_status?: string | null;
// 处置操作回调
onStartRemediation?: () => void;
onPauseRemediation?: () => void;
onResumeRemediation?: () => void;
onCancelRemediation?: () => void;
onRegenerateResponse?: () => void;
}>;
// 设备详情
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>
);
// ============ 告警列表渲染(v1.3:可展开 AI 面板)============
const [expandedAlertId, setExpandedAlertId] = useState<string | null>(null);
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;
const hasAI = !!(a.matched_plan_snapshot || a.initial_response || a.remediation);
const isExpanded = expandedAlertId === a.id || (hasAI && !expandedAlertId);
return (
<div key={a.id} className="rounded-lg border transition-all"
style={{ background: 'rgba(15,23,42,0.6)', borderColor: 'rgba(148,163,184,0.15)' }}>
{/* Header:点击展开 */}
<div
className="p-3 cursor-pointer"
onClick={() => hasAI && setExpandedAlertId(isExpanded ? null : a.id)}
>
<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>
{hasAI && (
<span className="text-[9px] px-1 py-0.5 rounded bg-cyan-500/15 text-cyan-400 flex-shrink-0">
AI 预案
</span>
)}
{a.escalation_status && (
<span className="text-[9px] px-1 py-0.5 rounded bg-red-500/15 text-red-400 flex-shrink-0">
已升级
</span>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<span className="text-xs text-muted-foreground">{a.time}</span>
{hasAI && (
isExpanded
? <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" />
: <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
)}
</div>
</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>
{/* AI 面板展开区 */}
{isExpanded && hasAI && (
<div className="px-3 pb-3 pt-0 border-t border-slate-600/20"
style={{ background: 'rgba(8,15,28,0.4)' }}>
<AlertAIPanel
alertId={a.id}
matchedPlan={a.matched_plan_snapshot}
initialResponse={a.initial_response}
remediation={a.remediation}
onStartRemediation={a.onStartRemediation}
onPauseRemediation={a.onPauseRemediation}
onResumeRemediation={a.onResumeRemediation}
onCancelRemediation={a.onCancelRemediation}
onRegenerateResponse={a.onRegenerateResponse}
/>
</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>
</>
);
}