feat(k8s): F7 集群详情EventsTab真实后端对接 + go vet修复

1. EventsTab.tsx: DEMO_EVENTS硬编码→kubernetesApi.getEvents真实K8s事件
   - 复用 normalizeK8sEvent/formatEventRelative 工具(与 EventsPage 一致)
   - 新增 loading/empty 状态 + 刷新按钮
   - Warning事件优先排序 + 限100条防溢出
   - KubernetesClusterDetail 传入 clusterCode prop
   - 修复 Q28 组件拆分时遗留的 demo 数据未对接缺口

2. aiops_alert_analysis.go: log.Printf %s→%d 修复 go vet 失败
   - AlertID 类型为 uint, %s 格式符不匹配致 go test 构建失败
   - go vet ./... 全量零错误

验证: tsc -b=0, npm build✓ (18.00s), go build=0, go vet=0, 17/17测试通过
Events API 返回5条真实事件 (type=Normal, reason=Pulled, count=5487)
This commit is contained in:
attacker committed 2026-07-16 21:23:19 +08:00
1 parent a874832f10
commit ca036a85ca
4 files changed
+94 -20

No files matched your search

@@ -122,7 +122,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
<TabsContent value="deployments" className="mt-4"><DeploymentsTab deployments={deployments} /></TabsContent>
<TabsContent value="services" className="mt-4"><ServicesTab services={services} /></TabsContent>
<TabsContent value="ingresses" className="mt-4"><IngressesTab ingresses={ingresses} /></TabsContent>
<TabsContent value="events" className="mt-4"><EventsTab /></TabsContent>
<TabsContent value="events" className="mt-4"><EventsTab clusterCode={cluster?.code as string | undefined} /></TabsContent>
</Tabs>
</CardContent>
</Card>
@@ -1,41 +1,114 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { RefreshCw } from 'lucide-react';
import { kubernetesApi } from '@/api';
import { normalizeK8sEvent, formatEventRelative } from '../k8s-event-utils';
import type { NormalizedEvent } from '../k8s-event-utils';
import type { JsonData } from '@/types/common';
/** 集群事件 mock 数据 — 真正的事件需后端 events/summary 端点对接 */
const DEMO_EVENTS = [
{ type: 'Normal', reason: 'Started', message: 'Started container nginx' },
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx' },
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available' },
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"' },
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image' },
];
interface EventsTabProps {
clusterCode?: string;
}
const MIN_AGOS = [2, 5, 10, 15, 12];
export function EventsTab() {
export function EventsTab({ clusterCode }: EventsTabProps) {
const { t } = useTranslation();
const [events, setEvents] = useState<NormalizedEvent[]>([]);
const [loading, setLoading] = useState(false);
const fetchEvents = useCallback(async () => {
if (!clusterCode) return;
setLoading(true);
try {
const res = await kubernetesApi.getEvents(clusterCode);
const arr = Array.isArray(res) ? res : (res?.list || []);
const normalized = (arr as JsonData[]).map(normalizeK8sEvent);
// Warning 优先,然后按 lastTime 降序
normalized.sort((a, b) => {
if (a.type !== b.type) return a.type === 'Warning' ? -1 : 1;
return new Date(b.lastTime || 0).getTime() - new Date(a.lastTime || 0).getTime();
});
setEvents(normalized.slice(0, 100));
} catch {
setEvents([]);
} finally {
setLoading(false);
}
}, [clusterCode]);
useEffect(() => {
if (clusterCode) {
// eslint-disable-next-line react-hooks/set-state-in-effect
fetchEvents();
}
}, [clusterCode, fetchEvents]);
if (loading) {
return (
<div className="flex items-center justify-center py-8 text-[var(--dg-text-muted)]">
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
{t('common.loading')}
</div>
);
}
if (events.length === 0) {
return (
<div className="flex items-center justify-between">
<div className="py-8 text-center text-[var(--dg-text-muted)]">
{t('k8s.empty.events')}
</div>
{clusterCode && (
<Button variant="ghost" size="sm" onClick={fetchEvents} title={t('common.refresh')}>
<RefreshCw className="h-4 w-4" />
</Button>
)}
</div>
);
}
return (
<div className="space-y-2">
{DEMO_EVENTS.map((event, index) => (
<div className="flex justify-end mb-2">
<Button variant="ghost" size="sm" onClick={fetchEvents} title={t('common.refresh')}>
<RefreshCw className="h-3.5 w-3.5 mr-1" />
{t('common.refresh')}
</Button>
</div>
{events.map((event, index) => (
<div
key={index}
key={`${event.name}-${index}`}
className="flex items-center gap-3 p-3 rounded-lg bg-[var(--dg-bg-tertiary)]/50"
>
<Badge
variant="outline"
className={
event.type === 'Normal'
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10'
: 'border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10'
? 'border-[var(--dg-green)]/30 text-[var(--dg-green)] bg-[var(--dg-green)]/10 shrink-0'
: 'border-[var(--dg-orange)]/30 text-[var(--dg-orange)] bg-[var(--dg-orange)]/10 shrink-0'
}
>
{event.type}
</Badge>
<span className="text-sm font-medium text-[var(--dg-text-secondary)] min-w-[120px]">{event.reason}</span>
<span className="text-sm text-[var(--dg-text-secondary)] flex-1">{event.message}</span>
<span className="text-xs text-[var(--dg-text-muted)]">{t('common.duration.minAgo', { count: MIN_AGOS[index] })}</span>
<span className="text-sm font-medium text-[var(--dg-text-secondary)] min-w-[120px] shrink-0">{event.reason}</span>
<span className="text-sm text-[var(--dg-text-secondary)] flex-1 truncate" title={event.message}>{event.message}</span>
{event.count > 1 && (
<span className="text-xs text-[var(--dg-text-muted)] shrink-0">×{event.count}</span>
)}
{event.object && event.object !== '-' && (
<span className="text-xs font-mono text-[var(--dg-text-muted)] shrink-0 max-w-[200px] truncate" title={event.object}>{event.object}</span>
)}
{event.lastTime && (
<span className="text-xs text-[var(--dg-text-muted)] shrink-0">{formatEventRelative(event.lastTime)}</span>
)}
</div>
))}
{events.length === 100 && (
<div className="text-xs text-center text-[var(--dg-text-muted)] py-2">
{t('k8s.event.totalCount', { count: 100 })}
</div>
)}
</div>
);
}