From f9e3a51dd5e92b55916d98a6718597a242606636 Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Fri, 13 Feb 2026 16:30:16 -0500 Subject: [PATCH 1/2] CONSOLE-5066: Refactor ResourceEventStream for Performance and Impersonation Support --- frontend/public/components/events.tsx | 122 ++++++-------------------- 1 file changed, 26 insertions(+), 96 deletions(-) diff --git a/frontend/public/components/events.tsx b/frontend/public/components/events.tsx index 129d87d72e..6253df75a9 100644 --- a/frontend/public/components/events.tsx +++ b/frontend/public/components/events.tsx @@ -1,6 +1,6 @@ import * as _ from 'lodash'; import type { ComponentType, FC, ReactNode } from 'react'; -import { useEffect, useState, useRef, useMemo } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { css } from '@patternfly/react-styles'; import { Link, useParams } from 'react-router-dom-v5-compat'; import { DocumentTitle } from '@console/shared/src/components/document-title/DocumentTitle'; @@ -26,11 +26,10 @@ import { isGroupVersionKind, kindForReference, referenceFor, - watchURL, } from '../module/k8s'; import { withStartGuide } from './start-guide'; -import { WSFactory } from '../module/ws-factory'; import { EventModel, NodeModel } from '../models'; +import { useK8sWatchResource } from './utils/k8s-watch-hook'; import { useFlag } from '@console/shared/src/hooks/flag'; import { FLAGS } from '@console/shared/src/constants/common'; import { PageHeading } from '@console/shared/src/components/heading/PageHeading'; @@ -49,14 +48,12 @@ import PaneBody from '@console/shared/src/components/layout/PaneBody'; import type { EventKind } from '../module/k8s/types'; import type { EventInvolvedObject } from '../module/k8s/event'; import type { CellMeasurerCache } from 'react-virtualized'; -import type { WSOptions } from '@console/dynamic-plugin-sdk/src/utils/k8s/ws-factory'; import type { K8sResourceCommon, ResourceEventStreamProps, } from '@console/dynamic-plugin-sdk/src/extensions/console-types'; const maxMessages = 500; -const flushInterval = 500; // Extended EventKind type to include reportingComponent field present in v1 events interface ExtendedEventKind extends EventKind { @@ -379,94 +376,31 @@ const EventStream: FC = ({ }) => { const { t } = useTranslation('public'); const [active, setActive] = useState(true); - const [sortedEvents, setSortedEvents] = useState([]); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - const ws = useRef(null); - const filteredEvents = useMemo(() => { - return filterEvents(sortedEvents, { kind, type, filter, textFilter }).slice(0, maxMessages); - }, [sortedEvents, kind, type, filter, textFilter]); + // Use the standard Kubernetes watch hook with list-then-watch logic + const [eventsData, eventsLoaded, eventsLoadError] = useK8sWatchResource( + !mock + ? { + isList: true, + kind: EventModel.kind, + namespace, + fieldSelector, + } + : null, + ); - // Handle websocket setup and teardown when dependent props change - useEffect(() => { - ws.current?.destroy(); - setSortedEvents([]); - if (!mock) { - const webSocketID = `${namespace || 'all'}-sysevents`; - const watchURLOptions = { - ...(namespace ? { ns: namespace } : {}), - ...(fieldSelector - ? { - queryParams: { - fieldSelector: encodeURIComponent(fieldSelector), - }, - } - : {}), - }; - const path = watchURL(EventModel, watchURLOptions); - const webSocketOptions: WSOptions = { - host: 'auto', - reconnect: true, - path, - subprotocols: [], - jsonParse: true, - bufferFlushInterval: flushInterval, - bufferMax: maxMessages, - }; - - ws.current = new WSFactory(webSocketID, webSocketOptions) - .onbulkmessage((messages: Array<{ object: EventKind; type: string }>) => { - // Make one update to state per batch of events. - setSortedEvents((currentSortedEvents) => { - const topEvents = currentSortedEvents.slice(0, maxMessages); - const batch = messages.reduce((acc, { object, type: eventType }) => { - const uid = object.metadata.uid; - switch (eventType) { - case 'ADDED': - case 'MODIFIED': - if (acc[uid] && acc[uid].count > object.count) { - // We already have a more recent version of this message stored, so skip this one - return acc; - } - return { ...acc, [uid]: object }; - case 'DELETED': - return _.omit(acc, uid); - default: - // eslint-disable-next-line no-console - console.error(`UNHANDLED EVENT: ${eventType}`); - return acc; - } - }, _.keyBy(topEvents, 'metadata.uid') as Record); - return sortEvents(batch); - }); - }) - .onopen(() => { - setError(false); - setLoading(false); - }) - .onclose((evt?: { wasClean?: boolean; reason?: string }) => { - if (evt?.wasClean === false) { - setError(evt.reason || t('Connection did not close cleanly.')); - } - }) - .onerror(() => { - setError(true); - }); + // Sort events and limit to maxMessages + // Note: We keep the events visible even when paused (active=false) + const sortedEvents = useMemo(() => { + if (!eventsData) { + return []; } - return () => { - ws.current?.destroy(); - }; - }, [namespace, fieldSelector, mock, t]); + return sortEvents(eventsData).slice(0, maxMessages); + }, [eventsData]); - // Pause/unpause the websocket when the active state changes - useEffect(() => { - if (active) { - ws.current?.unpause(); - } else { - ws.current?.pause(); - } - }, [active]); + const filteredEvents = useMemo(() => { + return filterEvents(sortedEvents, { kind, type, filter, textFilter }).slice(0, maxMessages); + }, [sortedEvents, kind, type, filter, textFilter]); const toggleStream = () => { setActive((prev) => !prev); @@ -486,18 +420,14 @@ const EventStream: FC = ({ sysEventStatus = ; } - if (error) { + if (eventsLoadError) { statusBtnTxt = ( - {typeof error === 'string' - ? t('Error connecting to event stream: {{ error }}', { - error, - }) - : t('Error connecting to event stream')} + {t('Error connecting to event stream')} ); sysEventStatus = ; - } else if (loading) { + } else if (!eventsLoaded) { statusBtnTxt = {t('Loading events...')}; sysEventStatus = ; } else if (active) { From 715b4647e9c1807b7f49660b23c79693563cc8b9 Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Mon, 16 Feb 2026 00:00:17 -0500 Subject: [PATCH 2/2] Yarn i18n --- frontend/public/locales/en/public.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/public/locales/en/public.json b/frontend/public/locales/en/public.json index a93312ea78..60d0168ca0 100644 --- a/frontend/public/locales/en/public.json +++ b/frontend/public/locales/en/public.json @@ -574,8 +574,6 @@ "{{count}} event exist, but none match the current filter_other": "{{count}} event exist, but none match the current filters", "Error loading events": "Error loading events", "An error occurred during event retrieval. Attempting to reconnect...": "An error occurred during event retrieval. Attempting to reconnect...", - "Connection did not close cleanly.": "Connection did not close cleanly.", - "Error connecting to event stream: {{ error }}": "Error connecting to event stream: {{ error }}", "Error connecting to event stream": "Error connecting to event stream", "Loading events...": "Loading events...", "Streaming events...": "Streaming events...",