Files
aiops/frontend/src/components/kubernetes/EventsPage.tsx
T
Logan.Li 339b24a579 优化 K8s 页面布局与 local 模式部署收尾
P0-P1:
- Dockerfile.embed 启用 CGO 支持 SQLite+redka
- K8s scale>1 时警告 local 模式数据不共享

K8s UI:
- 移除侧边栏资源用量/容量分析及资源页横向 Tab 栏
- 修复事件路由(/workloads/events)与 Deployment 事件过滤
- 网络/配置/存储统一 YamlViewer 编辑,加大编辑弹窗
2026-06-26 13:36:27 +08:00

446 lines
23 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 { useState, useEffect, useCallback, useMemo } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
Cell,
} from 'recharts';
import { RefreshCw, ChevronRight, Bell, AlertTriangle, Info, Activity, Server, Layers } from 'lucide-react';
import { kubernetesApi } from '@/api';
// Color palette for charts
const CHART_COLORS = ['#EF4444', '#F59E0B', '#3B82F6', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316', '#6366F1', '#84CC16', '#06B6D4'];
export default function EventsPage() {
const { clusterCode } = useParams<{ clusterCode: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<any[]>([]);
const [clusters, setClusters] = useState<any[]>([]);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [selectedNs, setSelectedNs] = useState('all');
const [filterType, setFilterType] = useState('all');
const [timeWindow, setTimeWindow] = useState('0'); // 0 = all
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
// Aggregation data
const [summary, setSummary] = useState<any>(null);
const [selectedReason, setSelectedReason] = useState<string>('');
const [selectedKind, setSelectedKind] = useState<string>('');
// Expanded row
const [expandedRow, setExpandedRow] = useState<string | null>(null);
useEffect(() => {
kubernetesApi.getClusters().then(( res: any) => {
const list = res?.list || [];
setClusters(list);
if (!activeCluster && list.length > 0) {
const code = list[0].code || String(list[0].id);
setActiveCluster(code);
navigate(`/kubernetes/${code}/events`, { replace: true });
}
}).catch(() => {});
}, []);
useEffect(() => {
if (!activeCluster) return;
kubernetesApi.getNamespaces(activeCluster).then(( res: any) => {
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
}).catch(() => {});
}, [activeCluster]);
const loadEvents = useCallback(async () => {
if (!activeCluster) return;
setLoading(true);
try {
const ns = selectedNs === 'all' ? undefined : selectedNs;
const ft = filterType === 'all' ? undefined : filterType;
const res: Record<string, any> = await kubernetesApi.getEvents(activeCluster, ns, ft);
setEvents(Array.isArray(res) ? res : (res?.list || []));
} catch { setEvents([]); } finally { setLoading(false); }
}, [activeCluster, selectedNs, filterType]);
const loadSummary = useCallback(async () => {
if (!activeCluster) return;
try {
const ns = selectedNs === 'all' ? undefined : selectedNs;
const hours = parseInt(timeWindow) || undefined;
const res: Record<string, any> = await kubernetesApi.getEventsSummary(activeCluster, ns, hours);
setSummary(res || null);
} catch { setSummary(null); }
}, [activeCluster, selectedNs, timeWindow]);
useEffect(() => { loadEvents(); }, [loadEvents]);
useEffect(() => { loadSummary(); }, [loadSummary]);
// Filter events by selected reason/kind from aggregation click
const displayEvents = useMemo(() => {
let filtered = events;
const hours = parseInt(timeWindow, 10);
if (hours > 0) {
const since = Date.now() - hours * 3600 * 1000;
filtered = filtered.filter((e) => {
const ts = e.last_time || e.lastTime || e.last_timestamp;
return !ts || new Date(ts).getTime() >= since;
});
}
if (selectedReason) {
filtered = filtered.filter(e => e.reason === selectedReason);
}
if (selectedKind) {
filtered = filtered.filter(e => (e.object || '').startsWith(selectedKind + '/'));
}
return filtered;
}, [events, selectedReason, selectedKind, timeWindow]);
const warningCount = useMemo(() => events.filter(e => e.type === 'Warning').length, [events]);
const normalCount = useMemo(() => events.filter(e => e.type === 'Normal').length, [events]);
// Chart data from summary
const reasonData = useMemo(() => {
const list = summary?.by_reason || [];
return list.map(( r: any) => ({ name: r.reason || 'Unknown', count: r.count }));
}, [summary]);
const nsData = useMemo(() => {
const list = summary?.by_namespace || [];
return list.map(( n: any) => ({ name: n.namespace || '<cluster>', count: n.count }));
}, [summary]);
const kindData = useMemo(() => {
const list = summary?.by_kind || [];
return list.map(( k: any) => ({ name: k.kind || 'Unknown', count: k.count }));
}, [summary]);
const formatTime = (ts: string) => {
if (!ts) return '-';
return ts.replace('T', ' ').substring(0, 19);
};
const formatRelative = (ts: string) => {
if (!ts) return '';
const diff = Date.now() - new Date(ts).getTime();
if (diff < 0) return '';
const mins = Math.floor(diff / 60000);
if (mins < 1) return '刚刚';
if (mins < 60) return `${mins}分钟前`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}小时前`;
return `${Math.floor(hrs / 24)}天前`;
};
return (
<div className="space-y-4">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-slate-500">
<Link to="/kubernetes" className="hover:text-slate-300">集群</Link>
{activeCluster && <><ChevronRight className="h-3 w-3" /><Link to={`/kubernetes/${activeCluster}`} className="hover:text-slate-300">{activeCluster}</Link></>}
<ChevronRight className="h-3 w-3" /><span className="text-slate-300">事件</span>
</div>
{/* Header + Filters */}
<div className="flex items-center justify-between flex-wrap gap-3">
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
<Bell className="h-5 w-5 text-yellow-400" />事件中心
</h2>
<div className="flex items-center gap-2 flex-wrap">
<Select value={activeCluster} onValueChange={v => { setActiveCluster(v); navigate(`/kubernetes/${v}/events`); }}>
<SelectTrigger className="w-40 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
{clusters.map(( c: any) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
</SelectContent>
</Select>
<Select value={selectedNs} onValueChange={v => { setSelectedNs(v); setSelectedReason(''); setSelectedKind(''); }}>
<SelectTrigger className="w-36 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue placeholder="命名空间" /></SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="all" className="text-xs">所有命名空间</SelectItem>
{namespaces.map(n => <SelectItem key={n} value={n} className="text-xs">{n}</SelectItem>)}
</SelectContent>
</Select>
<Select value={timeWindow} onValueChange={v => setTimeWindow(v)}>
<SelectTrigger className="w-32 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="0" className="text-xs">全部时间</SelectItem>
<SelectItem value="1" className="text-xs">近 1 小时</SelectItem>
<SelectItem value="6" className="text-xs">近 6 小时</SelectItem>
<SelectItem value="24" className="text-xs">近 24 小时</SelectItem>
<SelectItem value="168" className="text-xs">近 7 天</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm" className="h-8 border-slate-700 text-slate-300 text-xs" onClick={() => { loadEvents(); loadSummary(); }} disabled={loading}>
<RefreshCw className={`h-3 w-3 mr-1 ${loading ? 'animate-spin' : ''}`} />刷新
</Button>
</div>
</div>
{/* Summary Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500 mb-1">事件总数</p>
<p className="text-2xl font-bold text-slate-100">{summary?.total ?? events.length}</p>
</div>
<Activity className="h-8 w-8 text-slate-600" />
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-red-900/40">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500 mb-1">告警事件</p>
<p className="text-2xl font-bold text-red-400">{summary?.warning_count ?? warningCount}</p>
<p className="text-[10px] text-slate-600 mt-0.5">Warning 累计触发次数</p>
</div>
<AlertTriangle className="h-8 w-8 text-red-500/30" />
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-emerald-900/40">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500 mb-1">正常事件</p>
<p className="text-2xl font-bold text-emerald-400">{summary?.normal_count ?? normalCount}</p>
</div>
<Info className="h-8 w-8 text-emerald-500/30" />
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500 mb-1">告警类型数</p>
<p className="text-2xl font-bold text-amber-400">{reasonData.length}</p>
<p className="text-[10px] text-slate-600 mt-0.5">不同 Reason 数量</p>
</div>
<Layers className="h-8 w-8 text-amber-500/30" />
</div>
</CardContent>
</Card>
</div>
{/* Active Filter Badge */}
{(selectedReason || selectedKind) && (
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">已选过滤:</span>
{selectedReason && (
<Badge className="bg-amber-500/20 text-amber-300 border-amber-500/30 cursor-pointer text-xs" onClick={() => setSelectedReason('')}>
原因: {selectedReason} ✕
</Badge>
)}
{selectedKind && (
<Badge className="bg-blue-500/20 text-blue-300 border-blue-500/30 cursor-pointer text-xs" onClick={() => setSelectedKind('')}>
类型: {selectedKind} ✕
</Badge>
)}
</div>
)}
{/* Charts Row */}
{reasonData.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* By Reason Chart */}
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<h3 className="text-sm font-medium text-slate-300 mb-3 flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-amber-400" />告警原因 Top {reasonData.length}
</h3>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={reasonData} layout="vertical" margin={{ left: 10, right: 10, top: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" horizontal={false} />
<XAxis type="number" tick={{ fill: '#64748b', fontSize: 10 }} axisLine={{ stroke: '#334155' }} />
<YAxis type="category" dataKey="name" tick={{ fill: '#94a3b8', fontSize: 10 }} width={100} axisLine={{ stroke: '#334155' }} />
<Tooltip
contentStyle={{ background: '#1e293b', border: '1px solid #334155', borderRadius: '6px', fontSize: '12px' }}
labelStyle={{ color: '#e2e8f0' }}
/>
<Bar dataKey="count" radius={[0, 4, 4, 0]} cursor="pointer" onClick={( d: any) => setSelectedReason(prev => prev === d?.name ? '' : d?.name)}>
{reasonData.map(( _: any, i: number) => (
<Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
<p className="text-[10px] text-slate-600 text-center mt-1">点击柱状图过滤事件列表</p>
</CardContent>
</Card>
{/* By Namespace Chart */}
{nsData.length > 0 && (
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<h3 className="text-sm font-medium text-slate-300 mb-3 flex items-center gap-1.5">
<Server className="h-4 w-4 text-blue-400" />命名空间分布 Top {nsData.length}
</h3>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={nsData} layout="vertical" margin={{ left: 10, right: 10, top: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" horizontal={false} />
<XAxis type="number" tick={{ fill: '#64748b', fontSize: 10 }} axisLine={{ stroke: '#334155' }} />
<YAxis type="category" dataKey="name" tick={{ fill: '#94a3b8', fontSize: 10 }} width={80} axisLine={{ stroke: '#334155' }} />
<Tooltip
contentStyle={{ background: '#1e293b', border: '1px solid #334155', borderRadius: '6px', fontSize: '12px' }}
labelStyle={{ color: '#e2e8f0' }}
/>
<Bar dataKey="count" radius={[0, 4, 4, 0]} fill="#3B82F6" fillOpacity={0.7} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
)}
{/* By Kind Chart */}
{kindData.length > 0 && (
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<h3 className="text-sm font-medium text-slate-300 mb-3 flex items-center gap-1.5">
<Layers className="h-4 w-4 text-purple-400" />资源类型分布
</h3>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={kindData} layout="vertical" margin={{ left: 10, right: 10, top: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" horizontal={false} />
<XAxis type="number" tick={{ fill: '#64748b', fontSize: 10 }} axisLine={{ stroke: '#334155' }} />
<YAxis type="category" dataKey="name" tick={{ fill: '#94a3b8', fontSize: 10 }} width={80} axisLine={{ stroke: '#334155' }} />
<Tooltip
contentStyle={{ background: '#1e293b', border: '1px solid #334155', borderRadius: '6px', fontSize: '12px' }}
labelStyle={{ color: '#e2e8f0' }}
/>
<Bar dataKey="count" radius={[0, 4, 4, 0]} cursor="pointer" onClick={( d: any) => setSelectedKind(prev => prev === d?.name ? '' : d?.name)}>
{kindData.map(( _: any, i: number) => (
<Cell key={i} fill={['#8B5CF6', '#6366F1', '#EC4899', '#14B8A6'][i % 4]} fillOpacity={0.7} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
<p className="text-[10px] text-slate-600 text-center mt-1">点击柱状图过滤事件列表</p>
</CardContent>
</Card>
)}
</div>
)}
{/* Recent Warnings Alert Panel */}
{summary?.recent_warnings?.length > 0 && (
<Card className="bg-red-950/20 border-red-900/30">
<CardContent className="p-4">
<h3 className="text-sm font-medium text-red-300 mb-3 flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4" />最新告警事件 ({summary.recent_warnings.length})
</h3>
<div className="space-y-1.5">
{summary.recent_warnings.map(( w: any, i: number) => (
<div key={i} className="flex items-start gap-2 text-xs py-1.5 px-2 rounded bg-slate-900/40 hover:bg-slate-800/40 transition-colors">
<AlertTriangle className="h-3.5 w-3.5 text-red-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" className="text-[10px] h-4 px-1 border-red-500/30 text-red-400">{w.reason}</Badge>
<span className="text-slate-500 font-mono text-[11px]">{w.object}</span>
<span className="text-slate-600 text-[10px]">{w.namespace}</span>
<span className="text-slate-600 text-[10px]">×{w.count}</span>
<span className="text-slate-600 text-[10px] ml-auto">{formatRelative(w.last_time)}</span>
</div>
<p className="text-slate-400 mt-0.5 truncate" title={w.message}>{w.message}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Events Table */}
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-0">
<div className="flex items-center justify-between px-4 py-2 border-b border-slate-800">
<span className="text-sm text-slate-400">事件列表 ({displayEvents.length})</span>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-28 h-7 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="all" className="text-xs">所有类型</SelectItem>
<SelectItem value="Warning" className="text-xs">⚠ Warning</SelectItem>
<SelectItem value="Normal" className="text-xs">ℹ Normal</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader><TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400 w-16">类型</TableHead>
<TableHead className="text-slate-400 w-24">原因</TableHead>
<TableHead className="text-slate-400">对象</TableHead>
<TableHead className="text-slate-400">消息</TableHead>
<TableHead className="text-slate-400 w-12">次数</TableHead>
<TableHead className="text-slate-400 w-20">命名空间</TableHead>
<TableHead className="text-slate-400 w-40">最后时间</TableHead>
</TableRow></TableHeader>
<TableBody>
{displayEvents.length === 0 ? (
<TableRow><TableCell colSpan={7} className="text-center text-slate-500 py-8">
{loading ? <span className="animate-spin inline-block">⏳</span> : '暂无事件'}
</TableCell></TableRow>
) : displayEvents.slice(0, 200).map((e, i) => {
const rowKey = `${e.name}-${i}`;
const isExpanded = expandedRow === rowKey;
return (
<TableRow
key={rowKey}
className={`border-slate-800 cursor-pointer ${e.type === 'Warning' ? 'hover:bg-red-950/10' : 'hover:bg-slate-800/30'} ${isExpanded ? 'bg-slate-800/20' : ''}`}
onClick={() => setExpandedRow(isExpanded ? null : rowKey)}
>
<TableCell>
{e.type === 'Warning' ? (
<Badge className="bg-red-500/20 text-red-400 border-red-500/30 text-xs"><AlertTriangle className="h-3 w-3 mr-0.5 inline" />W</Badge>
) : (
<Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30 text-xs"><Info className="h-3 w-3 mr-0.5 inline" />N</Badge>
)}
</TableCell>
<TableCell className="text-xs text-slate-300 font-medium">
<span className={e.type === 'Warning' ? 'text-amber-400' : 'text-slate-400'}>{e.reason || '-'}</span>
</TableCell>
<TableCell className="text-xs text-slate-500 font-mono">{e.object || '-'}</TableCell>
<TableCell className="text-xs text-slate-400 max-w-[400px]">
<div className={`truncate ${isExpanded ? 'whitespace-normal' : ''}`} title={!isExpanded ? e.message : undefined}>
{e.message || '-'}
</div>
{isExpanded && (
<div className="mt-1 pt-1 border-t border-slate-800 text-[10px] text-slate-600 space-y-0.5">
<div>首次出现: {formatTime(e.first_time)} ({formatRelative(e.first_time)})</div>
<div>最后出现: {formatTime(e.last_time)} ({formatRelative(e.last_time)})</div>
<div>事件名称: {e.name}</div>
</div>
)}
</TableCell>
<TableCell className="text-sm text-slate-400 text-center">
{e.count > 1 ? <Badge variant="outline" className="text-[10px] h-4 px-1">{e.count}</Badge> : <span className="text-slate-600">1</span>}
</TableCell>
<TableCell className="text-xs text-slate-500">{e.namespace || '-'}</TableCell>
<TableCell className="text-xs text-slate-500">
<div>{formatTime(e.last_time)}</div>
<div className="text-[10px] text-slate-600">{formatRelative(e.last_time)}</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{displayEvents.length > 200 && (
<div className="text-center py-2 text-xs text-slate-600 border-t border-slate-800">
仅显示前 200 条,共 {displayEvents.length} 条事件
</div>
)}
</CardContent>
</Card>
</div>
);
}