From b2939470f407fea851ce21c3f0d17f919968974b Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Fri, 26 Jun 2026 15:13:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(k8s):=20=E4=BA=8B=E4=BB=B6=E9=A1=B5?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E6=97=B6=E9=97=B4/=E6=AC=A1=E6=95=B0/?= =?UTF-8?q?=E6=9D=A5=E6=BA=90=E7=AD=89=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端解析 source/reportingComponent 与 object_kind/name - 新增 EventTable 统一展示:最后/首次时间、次数、重复跨度、类型、原因、对象、来源、消息 - Deployment 事件 Tab 与事件中心共用表格组件 --- backend/controller/k8s_events.go | 101 ++++++++++---- backend/pkg/kubernetesclient/client.go | 26 ++-- .../components/kubernetes/DeploymentView.tsx | 11 +- .../src/components/kubernetes/EventTable.tsx | 124 ++++++++++++++++++ .../src/components/kubernetes/EventsPage.tsx | 107 +++------------ .../kubernetes/deployment-view/EventsTab.tsx | 48 +------ .../kubernetes/deployment-view/types.ts | 5 +- .../components/kubernetes/k8s-event-utils.ts | 77 +++++++++++ 8 files changed, 323 insertions(+), 176 deletions(-) create mode 100644 frontend/src/components/kubernetes/EventTable.tsx create mode 100644 frontend/src/components/kubernetes/k8s-event-utils.ts diff --git a/backend/controller/k8s_events.go b/backend/controller/k8s_events.go index 37a804a..27ce6e3 100644 --- a/backend/controller/k8s_events.go +++ b/backend/controller/k8s_events.go @@ -7,6 +7,7 @@ import ( "time" "ezops/model" + k8sclient "ezops/pkg/kubernetesclient" "github.com/gin-gonic/gin" ) @@ -23,31 +24,42 @@ func (h *KubernetesHandler) ListEvents(c *gin.Context) { if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return } type EventSummary struct { - Namespace string `json:"namespace"` - Name string `json:"name"` - Type string `json:"type"` - Reason string `json:"reason"` - Message string `json:"message"` - Object string `json:"object"` - Count int `json:"count"` - FirstTime string `json:"first_time"` - LastTime string `json:"last_time"` + Namespace string `json:"namespace"` + Name string `json:"name"` + Type string `json:"type"` + Reason string `json:"reason"` + Message string `json:"message"` + Object string `json:"object"` + ObjectKind string `json:"object_kind"` + ObjectName string `json:"object_name"` + Source string `json:"source"` + Count int `json:"count"` + FirstTime string `json:"first_time"` + LastTime string `json:"last_time"` + EventTime string `json:"event_time"` } result := []EventSummary{} for _, e := range events.Items { if eventType != "" && e.Type != eventType { continue } - object := fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name) + objectKind := e.InvolvedObject.Kind + objectName := e.InvolvedObject.Name + object := fmt.Sprintf("%s/%s", objectKind, objectName) if e.InvolvedObject.Namespace != "" { - object = fmt.Sprintf("%s/%s/%s", e.InvolvedObject.Namespace, e.InvolvedObject.Kind, e.InvolvedObject.Name) + object = fmt.Sprintf("%s/%s/%s", e.InvolvedObject.Namespace, objectKind, objectName) + } + firstTime, lastTime, eventTime := formatEventTimes(e) + count := int(e.Count) + if count <= 0 { + count = 1 } - firstTime := ""; if !e.FirstTimestamp.IsZero() { firstTime = e.FirstTimestamp.Format(time.RFC3339) } - lastTime := ""; if !e.LastTimestamp.IsZero() { lastTime = e.LastTimestamp.Format(time.RFC3339) } result = append(result, EventSummary{ Namespace: e.Metadata.Namespace, Name: e.Metadata.Name, Type: e.Type, Reason: e.Reason, - Message: e.Message, Object: object, Count: int(e.Count), - FirstTime: firstTime, LastTime: lastTime, + Message: e.Message, Object: object, ObjectKind: objectKind, ObjectName: objectName, + Source: formatEventSource(e), Count: count, + FirstTime: firstTime, LastTime: lastTime, EventTime: eventTime, }) } + sort.Slice(result, func(i, j int) bool { return result[i].LastTime > result[j].LastTime }) c.JSON(200, model.Response{Code: 200, Data: result}) } @@ -122,15 +134,20 @@ func (h *KubernetesHandler) GetEventsSummary(c *gin.Context) { // Collect recent warning events (top 10 by lastTimestamp) if len(recentWarnings) < 50 { + firstTime, lastTime, eventTime := formatEventTimes(e) recentWarnings = append(recentWarnings, map[string]interface{}{ - "namespace": nsName, - "type": e.Type, - "reason": e.Reason, - "message": e.Message, - "object": fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name), - "count": count, - "last_time": e.LastTimestamp.Format(time.RFC3339), - "first_time": e.FirstTimestamp.Format(time.RFC3339), + "namespace": nsName, + "type": e.Type, + "reason": e.Reason, + "message": e.Message, + "object": fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name), + "object_kind": e.InvolvedObject.Kind, + "object_name": e.InvolvedObject.Name, + "source": formatEventSource(e), + "count": count, + "last_time": lastTime, + "first_time": firstTime, + "event_time": eventTime, }) } } else { @@ -200,6 +217,44 @@ func (h *KubernetesHandler) GetEventsSummary(c *gin.Context) { c.JSON(200, model.Response{Code: 200, Data: result}) } +// formatEventSource 格式化事件来源(component / reportingComponent)// Logan.Li +func formatEventSource(e k8sclient.EventItem) string { + if e.ReportingComponent != "" { + if e.ReportingInstance != "" { + return e.ReportingComponent + "/" + e.ReportingInstance + } + return e.ReportingComponent + } + src := e.Source.Component + if e.Source.Host != "" { + if src != "" { + return src + "@" + e.Source.Host + } + return e.Source.Host + } + return src +} + +// formatEventTimes 提取首次/最后/事件时间(RFC3339)// Logan.Li +func formatEventTimes(e k8sclient.EventItem) (first, last, eventTime string) { + if !e.FirstTimestamp.IsZero() { + first = e.FirstTimestamp.Format(time.RFC3339) + } + if !e.LastTimestamp.IsZero() { + last = e.LastTimestamp.Format(time.RFC3339) + } + if !e.EventTime.IsZero() { + eventTime = e.EventTime.Format(time.RFC3339) + } + if last == "" && eventTime != "" { + last = eventTime + } + if first == "" && eventTime != "" { + first = eventTime + } + return first, last, eventTime +} + // ============================================================================ // Capacity Analysis — CPU/Memory request/limit per Deployment // ============================================================================ diff --git a/backend/pkg/kubernetesclient/client.go b/backend/pkg/kubernetesclient/client.go index ea735c3..dfda7a7 100644 --- a/backend/pkg/kubernetesclient/client.go +++ b/backend/pkg/kubernetesclient/client.go @@ -1463,16 +1463,24 @@ type EventList struct { Items []EventItem `json:"items"` } +type EventSource struct { + Component string `json:"component"` + Host string `json:"host"` +} + type EventItem struct { - Metadata ObjectMeta `json:"metadata"` - InvolvedObject InvolvedObject `json:"involvedObject"` - Type string `json:"type"` - Reason string `json:"reason"` - Message string `json:"message"` - Count int32 `json:"count"` - FirstTimestamp time.Time `json:"firstTimestamp"` - LastTimestamp time.Time `json:"lastTimestamp"` - EventTime time.Time `json:"eventTime"` + Metadata ObjectMeta `json:"metadata"` + InvolvedObject InvolvedObject `json:"involvedObject"` + Type string `json:"type"` + Reason string `json:"reason"` + Message string `json:"message"` + Source EventSource `json:"source"` + ReportingComponent string `json:"reportingComponent"` + ReportingInstance string `json:"reportingInstance"` + Count int32 `json:"count"` + FirstTimestamp time.Time `json:"firstTimestamp"` + LastTimestamp time.Time `json:"lastTimestamp"` + EventTime time.Time `json:"eventTime"` } type InvolvedObject struct { diff --git a/frontend/src/components/kubernetes/DeploymentView.tsx b/frontend/src/components/kubernetes/DeploymentView.tsx index 9285f5e..dd39746 100644 --- a/frontend/src/components/kubernetes/DeploymentView.tsx +++ b/frontend/src/components/kubernetes/DeploymentView.tsx @@ -13,6 +13,7 @@ import { kubernetesApi } from '@/api'; import { useToast } from '@/hooks/use-toast'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { formatK8sTime, STATUS_COLORS } from './k8s-yaml-utils'; +import { normalizeK8sEvent } from './k8s-event-utils'; import { InfoItem, PodConditionsRow } from './deployment-view/helpers'; import { PodsTab } from './deployment-view/PodsTab'; @@ -111,15 +112,7 @@ export function DeploymentView(props: DeploymentViewProps) { if (obj.endsWith(`/${depName}`)) return true; const name = obj.split('/').pop() || ''; return name === depName || name.startsWith(`${depName}-`); - }).map((e: any) => ({ - type: e.type || 'Normal', - reason: e.reason || '', - message: e.message || '', - source: e.object || e.source?.component || e.sourceComponent || '-', - firstTimestamp: e.first_time || e.firstTimestamp || e.first_timestamp || '', - lastTimestamp: e.last_time || e.lastTimestamp || e.last_timestamp || '', - count: e.count || 1, - })); + }).map((e: any) => normalizeK8sEvent(e)); setEvents(depEvents); }).catch(() => {}).finally(() => setTabLoading('')); }, [tab, clusterCode, namespace, deployment?.name]); diff --git a/frontend/src/components/kubernetes/EventTable.tsx b/frontend/src/components/kubernetes/EventTable.tsx new file mode 100644 index 0000000..958895a --- /dev/null +++ b/frontend/src/components/kubernetes/EventTable.tsx @@ -0,0 +1,124 @@ +// EventTable — K8s 事件列表(时间/次数/类型/原因/对象/来源/消息)// Logan.Li +import { useState } from 'react'; +import { Badge } from '@/components/ui/badge'; +import { + Table, TableBody, TableCell, TableHead, TableHeader, TableRow, +} from '@/components/ui/table'; +import { Loader2 } from 'lucide-react'; +import { + type NormalizedEvent, + formatEventRepeatSpan, + formatEventTimeCell, +} from './k8s-event-utils'; + +interface EventTableProps { + events: NormalizedEvent[]; + loading?: boolean | string; + emptyText?: string; + maxRows?: number; + showNamespace?: boolean; +} + +export function EventTable({ + events, + loading = false, + emptyText = '暂无事件', + maxRows, + showNamespace = false, +}: EventTableProps) { + const [expandedRow, setExpandedRow] = useState(null); + const isLoading = loading === true || loading === 'events'; + const rows = maxRows ? events.slice(0, maxRows) : events; + const colSpan = showNamespace ? 10 : 9; + + return ( +
+ + + + 最后时间 + 首次时间 + 次数 + 重复 + 类型 + 原因 + 对象 + {showNamespace && 命名空间} + 来源 + 消息 + + + + {isLoading ? ( + + + + + + ) : rows.length === 0 ? ( + + {emptyText} + + ) : rows.map((e, i) => { + const rowKey = `${e.name || e.object}-${i}`; + const isExpanded = expandedRow === rowKey; + const isWarning = e.type === 'Warning'; + const last = formatEventTimeCell(e.lastTime); + const first = formatEventTimeCell(e.firstTime); + return ( + setExpandedRow(isExpanded ? null : rowKey)} + > + +
{last.absolute}
+
{last.relative}
+
+ +
{first.absolute}
+
{first.relative}
+
+ {e.count} + {formatEventRepeatSpan(e.firstTime, e.lastTime, e.count)} + + + {e.type} + + + {e.reason} + + {e.objectKind ? ( + {e.objectKind}/{e.objectName || e.object.split('/').pop()} + ) : e.object} + + {showNamespace && {e.namespace || '-'}} + {e.source} + +
+ {e.message} +
+ {isExpanded && e.name && ( +
+ 事件名: {e.name} +
+ )} +
+
+ ); + })} +
+
+ {maxRows && events.length > maxRows && ( +
+ 仅显示前 {maxRows} 条,共 {events.length} 条 +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/kubernetes/EventsPage.tsx b/frontend/src/components/kubernetes/EventsPage.tsx index 1bfd9d8..bf02fdb 100644 --- a/frontend/src/components/kubernetes/EventsPage.tsx +++ b/frontend/src/components/kubernetes/EventsPage.tsx @@ -5,11 +5,10 @@ import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - Table, TableBody, TableCell, TableHead, TableHeader, TableRow, -} from '@/components/ui/table'; import { RefreshCw, ChevronRight, Bell, AlertTriangle, X } from 'lucide-react'; import { kubernetesApi } from '@/api'; +import { EventTable } from './EventTable'; +import { normalizeK8sEvent, formatEventRelative } from './k8s-event-utils'; interface RankItem { name: string; count: number } @@ -87,7 +86,7 @@ export default function EventsPage() { const [summary, setSummary] = useState(null); const [selectedReason, setSelectedReason] = useState(''); const [selectedKind, setSelectedKind] = useState(''); - const [expandedRow, setExpandedRow] = useState(null); + useEffect(() => { kubernetesApi.getClusters().then((res: any) => { @@ -133,17 +132,18 @@ export default function EventsPage() { useEffect(() => { loadSummary(); }, [loadSummary]); const displayEvents = useMemo(() => { - let filtered = events; + 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) => { - const ts = e.last_time || e.lastTime || e.last_timestamp; - return !ts || new Date(ts).getTime() >= since; - }); + 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.object || '').startsWith(`${selectedKind}/`)); + if (selectedKind) { + filtered = filtered.filter((e) => + e.objectKind === selectedKind || e.object.startsWith(`${selectedKind}/`), + ); + } return filtered; }, [events, selectedReason, selectedKind, timeWindow]); @@ -162,20 +162,6 @@ export default function EventsPage() { (summary?.by_kind || []).map((k: any) => ({ name: k.kind || 'Unknown', count: k.count })), [summary]); - const formatTime = (ts: string) => (ts ? 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 (
@@ -280,8 +266,9 @@ export default function EventsPage() { {w.reason} {w.object} {w.namespace} + {w.source && {w.source}} ×{w.count} - {formatRelative(w.last_time)} + {formatEventRelative(w.last_time)}

{w.message}

@@ -304,70 +291,12 @@ export default function EventsPage() { - - - - 类型 - 原因 - 对象 - 消息 - 次数 - 命名空间 - 最后时间 - - - - {displayEvents.length === 0 ? ( - - - {loading ? : '暂无事件'} - - - ) : displayEvents.slice(0, 200).map((e, i) => { - const rowKey = `${e.name}-${i}`; - const isExpanded = expandedRow === rowKey; - const isWarning = e.type === 'Warning'; - return ( - setExpandedRow(isExpanded ? null : rowKey)} - > - - - {isWarning ? 'Warning' : 'Normal'} - - - {e.reason || '-'} - {e.object || '-'} - -
- {e.message || '-'} -
- {isExpanded && ( -
-
首次: {formatTime(e.first_time)} ({formatRelative(e.first_time)})
-
最后: {formatTime(e.last_time)} ({formatRelative(e.last_time)})
-
名称: {e.name}
-
- )} -
- {e.count > 1 ? e.count : 1} - {e.namespace || '-'} - -
{formatTime(e.last_time)}
-
{formatRelative(e.last_time)}
-
-
- ); - })} -
-
- {displayEvents.length > 200 && ( -
- 仅显示前 200 条,共 {displayEvents.length} 条 -
- )} + diff --git a/frontend/src/components/kubernetes/deployment-view/EventsTab.tsx b/frontend/src/components/kubernetes/deployment-view/EventsTab.tsx index dc269ac..dd31a0e 100644 --- a/frontend/src/components/kubernetes/deployment-view/EventsTab.tsx +++ b/frontend/src/components/kubernetes/deployment-view/EventsTab.tsx @@ -1,49 +1,13 @@ // deployment-view/EventsTab.tsx — 事件 Tab // Logan.Li -import { Badge } from '@/components/ui/badge'; -import { - Table, TableBody, TableCell, TableHead, TableHeader, TableRow, -} from '@/components/ui/table'; -import { Loader2 } from 'lucide-react'; -import { formatK8sTime } from '../k8s-yaml-utils'; +import { EventTable } from '../EventTable'; import type { EventItem } from './types'; export function EventsTab({ events, loading }: { events: EventItem[]; loading: string }) { - if (loading === 'events') { - return
; - } - if (events.length === 0) { - return
暂无事件
; - } return ( -
- - - - 类型 - 原因 - 消息 - 来源 - 次数 - 最后时间 - - - - {events.map((e, i) => ( - - - - {e.type} - - - {e.reason} - {e.message} - {e.source} - {e.count} - {formatK8sTime(e.lastTimestamp)} - - ))} - -
-
+ ); } \ No newline at end of file diff --git a/frontend/src/components/kubernetes/deployment-view/types.ts b/frontend/src/components/kubernetes/deployment-view/types.ts index f8f0878..d721f7d 100644 --- a/frontend/src/components/kubernetes/deployment-view/types.ts +++ b/frontend/src/components/kubernetes/deployment-view/types.ts @@ -19,10 +19,7 @@ export interface IngItem { name: string; className: string; hosts: string[]; rules: any[]; age: string; } -export interface EventItem { - type: string; reason: string; message: string; source: string; - firstTimestamp: string; lastTimestamp: string; count: number; -} +export type EventItem = import('../k8s-event-utils').NormalizedEvent; export interface DeploymentViewProps { clusterCode: string; namespace: string; deployment: any; diff --git a/frontend/src/components/kubernetes/k8s-event-utils.ts b/frontend/src/components/kubernetes/k8s-event-utils.ts new file mode 100644 index 0000000..8ea9bbe --- /dev/null +++ b/frontend/src/components/kubernetes/k8s-event-utils.ts @@ -0,0 +1,77 @@ +// k8s-event-utils — K8s Event 字段归一化与展示 // Logan.Li +import { elapsedSeconds, formatDuration, formatK8sTime } from './k8s-yaml-utils'; + +export interface NormalizedEvent { + name: string; + namespace: string; + type: string; + reason: string; + message: string; + object: string; + objectKind: string; + objectName: string; + source: string; + count: number; + firstTime: string; + lastTime: string; + eventTime: string; +} + +/** 后端/API 原始事件 → 统一结构 // Logan.Li */ +export function normalizeK8sEvent(raw: Record): NormalizedEvent { + const objectKind = raw.object_kind || raw.objectKind || ''; + const objectName = raw.object_name || raw.objectName || ''; + const object = raw.object + || (objectKind && objectName ? `${objectKind}/${objectName}` : '') + || ''; + const firstTime = raw.first_time || raw.firstTime || raw.first_timestamp || raw.firstTimestamp || ''; + const lastTime = raw.last_time || raw.lastTime || raw.last_timestamp || raw.lastTimestamp || ''; + const eventTime = raw.event_time || raw.eventTime || ''; + const count = Number(raw.count) > 0 ? Number(raw.count) : 1; + const source = raw.source + || raw.source_component + || raw.sourceComponent + || (raw.source?.component + ? `${raw.source.component}${raw.source.host ? `@${raw.source.host}` : ''}` + : '') + || '-'; + + return { + name: raw.name || '', + namespace: raw.namespace || '', + type: raw.type || 'Normal', + reason: raw.reason || '-', + message: raw.message || '-', + object: object || '-', + objectKind, + objectName, + source: source || '-', + count, + firstTime: firstTime || eventTime, + lastTime: lastTime || eventTime, + eventTime, + }; +} + +export function formatEventRelative(ts: string): 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)}天前`; +} + +/** 重复跨度:首次 → 最后 // Logan.Li */ +export function formatEventRepeatSpan(first: string, last: string, count: number): string { + if (count <= 1) return '-'; + const span = formatDuration(elapsedSeconds(first, last)); + return span === '-' ? `×${count}` : `${span} · ×${count}`; +} + +export function formatEventTimeCell(ts: string): { absolute: string; relative: string } { + return { absolute: formatK8sTime(ts), relative: formatEventRelative(ts) }; +} \ No newline at end of file