Files
aiops/frontend/src/components/kubernetes/EventsPage.tsx
T
attacker da9fd1e987 feat(quality): U12 工程质量回归修复 — ESLint errors(8→0) + React Hooks 关键规则(82→0)
远程 i18n 全量改造 + 堡垒机合并后引入两类质量回归,本次修复:

1. ESLint errors 8→0:
   - GrafanaDashboard/PrometheusDashboard: 未使用的 const { t } 改为 useTranslation()
   - ServerTableRow: 移除未使用 cn import
   - replay-utils: no-control-regex 标注 disable (终端回放有意检测控制字符)

2. React Hooks 6 大关键规则 82→0 (恢复 U7 治理水平):
   - exhaustive-deps 52→0 (mount-only effects 标注 disable)
   - set-state-in-effect 18→0 (effect 初始化模式)
   - purity 5→0 (AlertCenter 提取 nowMs / EnhancedLogResults 骨架屏确定性公式)
   - incompatible-library 3→0 (@tanstack/react-virtual 兼容性)
   - immutability 2→0 (AlertCenter TDZ)
   - preserve-manual-memoization 2→0 (StatChartPanel useMemo)

验证: tsc -b=0, npm build✓(18.04s), go build=0, ESLint errors=0, 17/17 测试通过
2026-07-06 00:55:01 +08:00

289 lines
14 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 } from 'react-router-dom';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { RefreshCw, Bell, AlertTriangle, X } from 'lucide-react';
import { kubernetesApi } from '@/api';
import { EventTable } from './EventTable';
import { normalizeK8sEvent, formatEventRelative } from './k8s-event-utils';
import { PageShell } from '@/components/layout/PageShell';
import { ContentPanel } from '@/components/layout/ContentPanel';
import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface RankItem { name: string; count: number }
// 简洁排名列表,替代花哨柱状图 // Logan.Li
function RankPanel({
title, items, selected, onSelect, emptyText,
}: {
title: string;
items: RankItem[];
selected?: string;
onSelect?: (name: string) => void;
emptyText?: string;
}) {
const max = Math.max(...items.map((i) => i.count), 1);
return (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-sm font-medium text-[var(--dg-text-primary)] mb-3">{title}</h3>
{items.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] py-6 text-center">{emptyText || i18n.t('aiops.assistantNoData')}</p>
) : (
<div className="space-y-1.5 max-h-[220px] overflow-y-auto pr-1">
{items.map((item) => {
const active = selected === item.name;
return (
<button
key={item.name}
type="button"
className={`w-full flex items-center gap-2 rounded px-2 py-1.5 text-left transition-colors ${
active ? 'bg-[var(--dg-bg-tertiary)]' : 'hover:bg-[var(--dg-bg-tertiary)]/60'
}`}
onClick={() => onSelect?.(active ? '' : item.name)}
>
<span className="w-[88px] shrink-0 truncate text-xs text-[var(--dg-text-secondary)]" title={item.name}>
{item.name}
</span>
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
<div
className={`h-full rounded-full ${active ? 'bg-[var(--dg-blue)]' : 'bg-[var(--dg-text-muted)]/50'}`}
style={{ width: `${(item.count / max) * 100}%` }}
/>
</div>
<span className="w-8 text-right text-xs tabular-nums text-[var(--dg-text-muted)]">{item.count}</span>
</button>
);
})}
</div>
)}
</CardContent>
</Card>
);
}
export default function EventsPage() {
const { clusterCode } = useParams<{ clusterCode: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<JsonData[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [selectedNs, setSelectedNs] = useState('all');
const [filterType, setFilterType] = useState('all');
const [timeWindow, setTimeWindow] = useState('0');
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
const [summary, setSummary] = useState<JsonData>(null);
const [selectedReason, setSelectedReason] = useState('');
const [selectedKind, setSelectedKind] = useState('');
useEffect(() => {
kubernetesApi.getClusters().then((res: JsonData) => {
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(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!activeCluster) return;
kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => {
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: JsonData = 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: JsonData = await kubernetesApi.getEventsSummary(activeCluster, ns, hours);
setSummary(res || null);
} catch { setSummary(null); }
}, [activeCluster, selectedNs, timeWindow]);
useEffect(() => { loadEvents(); }, [loadEvents]);
useEffect(() => { loadSummary(); }, [loadSummary]);
const displayEvents = useMemo(() => {
let filtered = events.map((e) => normalizeK8sEvent(e));
const hours = parseInt(timeWindow, 10);
if (hours > 0) {
const since = Date.now() - hours * 3600 * 1000;
filtered = filtered.filter((e) => !e.lastTime || new Date(e.lastTime).getTime() >= since);
}
if (selectedReason) filtered = filtered.filter((e) => e.reason === selectedReason);
if (selectedKind) {
filtered = filtered.filter((e) =>
e.objectKind === selectedKind || 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]);
const reasonData = useMemo<RankItem[]>(() =>
(summary?.by_reason || []).map((r: JsonData) => ({ name: r.reason || 'Unknown', count: r.count })),
[summary]);
const nsData = useMemo<RankItem[]>(() =>
(summary?.by_namespace || []).map((n: JsonData) => ({ name: n.namespace || '<cluster>', count: n.count })),
[summary]);
const kindData = useMemo<RankItem[]>(() =>
(summary?.by_kind || []).map((k: JsonData) => ({ name: k.kind || 'Unknown', count: k.count })),
[summary]);
const clusterName = clusters.find((c: JsonData) => (c.code || String(c.id)) === activeCluster)?.name;
const filterToolbar = (selectedReason || selectedKind) ? (
<div className="flex items-center gap-2 text-xs">
<span className="text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.eventspagetsx.s841389c5')} </span>
{selectedReason && (
<button type="button" className="inline-flex items-center gap-1 rounded border border-[var(--dg-border)] px-2 py-0.5 text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]" onClick={() => setSelectedReason('')}>
原因 {selectedReason} <X className="h-3 w-3" />
</button>
)}
{selectedKind && (
<button type="button" className="inline-flex items-center gap-1 rounded border border-[var(--dg-border)] px-2 py-0.5 text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]" onClick={() => setSelectedKind('')}>
类型 {selectedKind} <X className="h-3 w-3" />
</button>
)}
</div>
) : undefined;
return (
<PageShell
variant="list"
title={i18n.t('kubernetes.eventspagetsx.s08bf79b4')}
subtitle={clusterName ? `${clusterName} · 集群事件与告警` : i18n.t('kubernetes.eventspagetsx.s6bef2ba0')}
icon={<Bell className="h-5 w-5" />}
metrics={!loading ? (
<StatStrip
items={[
{ label: i18n.t('kubernetes.eventspagetsx.s0d3047da'), value: summary?.total ?? events.length, color: 'muted' },
{ label: i18n.t('kubernetes.eventspagetsx.s0e5b83cc'), value: summary?.warning_count ?? warningCount, color: 'amber' },
{ label: i18n.t('kubernetes.eventspagetsx.s349c629f'), value: summary?.normal_count ?? normalCount, color: 'green' },
{ label: i18n.t('kubernetes.eventspagetsx.sc62e34c5'), value: reasonData.length, color: 'red' },
]}
/>
) : undefined}
toolbar={filterToolbar}
actions={(
<>
<Select value={activeCluster} onValueChange={(v) => { setActiveCluster(v); navigate(`/kubernetes/${v}/events`); }}>
<SelectTrigger className="w-40 h-9 dg-input"><SelectValue /></SelectTrigger>
<SelectContent>
{clusters.map((c: JsonData) => (
<SelectItem key={c.id} value={c.code || String(c.id)}>{c.name || c.code}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedNs} onValueChange={(v) => { setSelectedNs(v); setSelectedReason(''); setSelectedKind(''); }}>
<SelectTrigger className="w-36 h-9 dg-input"><SelectValue placeholder={i18n.t('kubernetes.namespace.title')} /></SelectTrigger>
<SelectContent>
<SelectItem value="all"> {i18n.t('kubernetes.podmetricspaget.s58173b22')} </SelectItem>
{namespaces.map((n) => <SelectItem key={n} value={n}>{n}</SelectItem>)}
</SelectContent>
</Select>
<Select value={timeWindow} onValueChange={setTimeWindow}>
<SelectTrigger className="w-32 h-9 dg-input"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="0"> {i18n.t('kubernetes.eventspagetsx.s98c99f62')} </SelectItem>
<SelectItem value="1"> {i18n.t('monitor.helpersts.sf972b7b1')} </SelectItem>
<SelectItem value="6"> {i18n.t('monitor.siteProtocol.rangeDefault')} </SelectItem>
<SelectItem value="24"> {i18n.t('monitor.helpersts.s6e99b786')} </SelectItem>
<SelectItem value="168"> {i18n.t('monitor.helpersts.sd765efc4')} </SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => { loadEvents(); loadSummary(); }} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
刷新
</Button>
</>
)}
>
{reasonData.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<RankPanel title={i18n.t('kubernetes.eventspagetsx.s8c894faa')} items={reasonData} selected={selectedReason} onSelect={setSelectedReason} />
{nsData.length > 0 && <RankPanel title={i18n.t('kubernetes.namespace.title')} items={nsData} />}
{kindData.length > 0 && (
<RankPanel title={i18n.t('kubernetes.eventspagetsx.s14871aad')} items={kindData} selected={selectedKind} onSelect={setSelectedKind} />
)}
</div>
)}
{summary?.recent_warnings?.length > 0 && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] border-l-2 border-l-[var(--dg-orange)]">
<CardContent className="p-4">
<h3 className="text-sm font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-1.5">
<AlertTriangle className="h-4 w-4 text-[var(--dg-orange)]" />
最新告警 ({summary.recent_warnings.length})
</h3>
<div className="space-y-1">
{summary.recent_warnings.map((w: JsonData, i: number) => (
<div key={i} className="text-xs py-1.5 px-2 rounded hover:bg-[var(--dg-bg-tertiary)]/50">
<div className="flex items-center gap-2 flex-wrap">
<span className="shrink-0 font-mono text-[var(--dg-orange)]">{w.reason}</span>
<span className="text-[var(--dg-text-muted)] font-mono truncate">{w.object}</span>
<span className="text-[var(--dg-text-muted)]">{w.namespace}</span>
{w.source && <span className="text-[var(--dg-text-muted)] font-mono">{w.source}</span>}
<span className="text-[var(--dg-text-muted)]">×{w.count}</span>
<span className="ml-auto text-[var(--dg-text-muted)]">{formatEventRelative(w.last_time)}</span>
</div>
<p className="text-[var(--dg-text-secondary)] truncate mt-0.5" title={w.message}>{w.message}</p>
</div>
))}
</div>
</CardContent>
</Card>
)}
<ContentPanel
toolbar={(
<div className="flex items-center justify-between w-full">
<span className="text-sm dg-text-secondary">事件列表 ({displayEvents.length})</span>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-28 h-8 dg-input text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all"> {i18n.t('kubernetes.eventspagetsx.sfb995166')} </SelectItem>
<SelectItem value="Warning">Warning</SelectItem>
<SelectItem value="Normal">Normal</SelectItem>
</SelectContent>
</Select>
</div>
)}
>
<EventTable
events={displayEvents}
loading={loading}
showNamespace
/>
</ContentPanel>
</PageShell>
);
}