Files
aiops/frontend/src/components/kubernetes/EventTable.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

176 lines
7.8 KiB
TypeScript

// EventTable — K8s 事件列表(时间/次数/类型/原因/对象/来源/消息)// Logan.Li
// v2: 虚拟滚动,移除 maxRows 限制,支持万级事件流畅渲染
import { useState, useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Badge } from '@/components/ui/badge';
import { Loader2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import {
type NormalizedEvent,
formatEventRepeatSpan,
formatEventTimeCell,
} from './k8s-event-utils';
interface EventTableProps {
events: NormalizedEvent[];
loading?: boolean | string;
emptyText?: string;
/** @deprecated 虚拟滚动后不再需要截断,保留参数向后兼容但忽略 */
maxRows?: number;
showNamespace?: boolean;
/** 虚拟滚动容器高度 */
scrollHeight?: number;
}
const ROW_HEIGHT = 52;
const COLS_NS = '148px 148px 64px 80px 80px 112px minmax(120px,1fr) 96px 112px minmax(200px,1.5fr)';
const COLS = '148px 148px 64px 80px 80px 112px minmax(120px,1fr) 112px minmax(200px,1.5fr)';
const MIN_W_NS = '1080px';
const MIN_W = '980px';
export function EventTable({
events,
loading = false,
emptyText,
showNamespace = false,
scrollHeight = 600,
}: EventTableProps) {
const { t } = useTranslation();
const [expandedRow, setExpandedRow] = useState<string | null>(null);
const isLoading = loading === true || loading === 'events';
const scrollRef = useRef<HTMLDivElement>(null);
const gridCols = showNamespace ? COLS_NS : COLS;
const minWidth = showNamespace ? MIN_W_NS : MIN_W;
const headers = showNamespace
? [t('k8s.event.colLast'), t('k8s.event.colFirst'), t('k8s.event.colCount'), t('k8s.event.colRepeat'), t('k8s.event.colType'), t('k8s.event.colReason'), t('k8s.event.colObject'), t('k8s.event.colNamespace'), t('k8s.event.colSource'), t('k8s.event.colMessage')]
: [t('k8s.event.colLast'), t('k8s.event.colFirst'), t('k8s.event.colCount'), t('k8s.event.colRepeat'), t('k8s.event.colType'), t('k8s.event.colReason'), t('k8s.event.colObject'), t('k8s.event.colSource'), t('k8s.event.colMessage')];
const resolvedEmpty = emptyText || t('k8s.event.empty');
// eslint-disable-next-line react-hooks/incompatible-library
const virtualizer = useVirtualizer({
count: events.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => ROW_HEIGHT,
overscan: 10,
});
const virtualRows = virtualizer.getVirtualItems();
const totalHeight = virtualizer.getTotalSize();
return (
<div>
{/* 表头 — sticky 固定 */}
<div
className="grid gap-2 px-4 py-2.5 border-b border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-[11px] font-medium text-[var(--dg-text-muted)] uppercase tracking-wide"
style={{ gridTemplateColumns: gridCols, minWidth }}
>
{headers.map((h, i) => (
<span key={i} className={i === 2 ? 'text-center' : ''}>{h}</span>
))}
</div>
{/* 虚拟滚动体 */}
<div ref={scrollRef} style={{ height: scrollHeight, overflow: 'auto' }} className="overflow-x-auto">
{isLoading ? (
<div className="flex items-center justify-center" style={{ height: scrollHeight }}>
<Loader2 className="h-5 w-5 animate-spin text-[var(--dg-text-muted)]" />
</div>
) : events.length === 0 ? (
<div className="flex items-center justify-center text-[var(--dg-text-muted)] py-10">{resolvedEmpty}</div>
) : (
<div style={{ height: totalHeight, position: 'relative', minWidth }}>
{virtualRows.map((vr) => {
const e = events[vr.index];
if (!e) return null;
const rowKey = `${e.name || e.object}-${vr.index}`;
const isExpanded = expandedRow === rowKey;
const isWarning = e.type === 'Warning';
const last = formatEventTimeCell(e.lastTime);
const first = formatEventTimeCell(e.firstTime);
return (
<div
key={vr.key}
data-index={vr.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${vr.start}px)`,
display: 'grid',
gridTemplateColumns: gridCols,
gap: '0.5rem',
}}
className={`px-4 py-2.5 border-b border-[var(--dg-border)] cursor-pointer transition-colors ${
isExpanded ? 'bg-[var(--dg-bg-tertiary)]/40' : 'hover:bg-[var(--dg-bg-tertiary)]/30'
}`}
onClick={() => setExpandedRow(isExpanded ? null : rowKey)}
>
{/* 最后时间 */}
<div className="text-xs text-[var(--dg-text-muted)]">
<div>{last.absolute}</div>
<div className="text-[10px]">{last.relative}</div>
</div>
{/* 首次时间 */}
<div className="text-xs text-[var(--dg-text-muted)]">
<div>{first.absolute}</div>
<div className="text-[10px]">{first.relative}</div>
</div>
{/* 次数 */}
<div className="text-sm text-center tabular-nums text-[var(--dg-text-secondary)]">{e.count}</div>
{/* 重复跨度 */}
<div className="text-xs text-[var(--dg-text-muted)]">{formatEventRepeatSpan(e.firstTime, e.lastTime, e.count)}</div>
{/* 类型 */}
<div>
<Badge
variant="outline"
className={isWarning
? 'text-[var(--dg-orange)] border-[var(--dg-orange)]/30 text-[10px]'
: 'text-[var(--dg-text-muted)] border-[var(--dg-border)] text-[10px]'}
>
{e.type}
</Badge>
</div>
{/* 原因 */}
<div className="text-xs font-medium text-[var(--dg-text-secondary)]">{e.reason}</div>
{/* 对象 */}
<div className="text-xs font-mono text-[var(--dg-text-muted)] truncate">
{e.objectKind ? (
<span><span className="text-[var(--dg-text-secondary)]">{e.objectKind}</span>/{e.objectName || e.object.split('/').pop()}</span>
) : e.object}
</div>
{/* 命名空间(可选) */}
{showNamespace && <div className="text-xs text-[var(--dg-text-muted)]">{e.namespace || '-'}</div>}
{/* 来源 */}
<div className="text-xs text-[var(--dg-text-muted)] font-mono">{e.source}</div>
{/* 消息 */}
<div className="text-xs text-[var(--dg-text-secondary)] max-w-[360px]">
<div className={isExpanded ? 'whitespace-normal break-words' : 'truncate'} title={!isExpanded ? e.message : undefined}>
{e.message}
</div>
{isExpanded && e.name && (
<div className="mt-1 pt-1 border-t border-[var(--dg-border)] text-[10px] text-[var(--dg-text-muted)]">
{t('k8s.event.nameLabel')} {e.name}
</div>
)}
</div>
</div>
);
})}
</div>
)}
</div>
{/* 行数统计 */}
{events.length > 0 && (
<div className="text-center py-2 text-xs text-[var(--dg-text-muted)] border-t border-[var(--dg-border)]">
{t('k8s.event.totalCount', { count: events.length })} {events.length > 500 ? `· ${t('k8s.event.virtualScroll')}` : ''}
</div>
)}
</div>
);
}