Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 26 additions & 96 deletions frontend/public/components/events.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -379,94 +376,31 @@ const EventStream: FC<EventStreamProps> = ({
}) => {
const { t } = useTranslation('public');
const [active, setActive] = useState(true);
const [sortedEvents, setSortedEvents] = useState<EventKind[]>([]);
const [error, setError] = useState<string | boolean | null>(null);
const [loading, setLoading] = useState(true);
const ws = useRef<WSFactory | null>(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<EventKind[]>(
!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<string, EventKind>);
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]);
Comment on lines +392 to +399
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pause/play toggle no longer controls data updates.

The active state and toggleStream function remain, but with useK8sWatchResource, the watch continues fetching updates regardless of the toggle state. Previously with WebSocket, pausing would stop receiving new events. Now, the "pause" button only changes the status text—the underlying data continues to update in the background.

This is a behavioral change that may confuse users who expect pausing to freeze the event list. Consider one of:

  1. Remove the toggle entirely if pause functionality isn't meaningful with list-then-watch semantics.
  2. Implement client-side freezing by capturing a snapshot of eventsData when paused:
🔧 Option 2: Client-side pause implementation
+ const [pausedSnapshot, setPausedSnapshot] = useState<EventKind[] | null>(null);
+
+ const toggleStream = () => {
+   setActive((prev) => {
+     const newActive = !prev;
+     if (!newActive) {
+       // Capture snapshot when pausing
+       setPausedSnapshot(eventsData ?? []);
+     } else {
+       // Clear snapshot when resuming
+       setPausedSnapshot(null);
+     }
+     return newActive;
+   });
+ };

  // Sort events and limit to maxMessages
- // Note: We keep the events visible even when paused (active=false)
  const sortedEvents = useMemo(() => {
-   if (!eventsData) {
+   const dataSource = pausedSnapshot ?? eventsData;
+   if (!dataSource) {
      return [];
    }
-   return sortEvents(eventsData).slice(0, maxMessages);
- }, [eventsData]);
+   return sortEvents(dataSource).slice(0, maxMessages);
+ }, [pausedSnapshot, eventsData]);

Also applies to: 405-407

🤖 Prompt for AI Agents
In `@frontend/public/components/events.tsx` around lines 392 - 399, The pause
toggle no longer stops updates because useK8sWatchResource keeps fetching;
either remove the toggle UI or implement client-side freezing: when toggling to
paused (active false) capture a snapshot (e.g., pausedEvents state) from
eventsData and have sortedEvents derive from pausedEvents when active is false
and from eventsData when active is true; update toggleStream to set active and
populate/clear pausedEvents accordingly, keep using sortEvents(...).slice(0,
maxMessages) on the chosen source (eventsData or pausedEvents) so the visible
list stops changing while paused.


// 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);
Expand All @@ -486,18 +420,14 @@ const EventStream: FC<EventStreamProps> = ({
sysEventStatus = <NoMatchingEvents allCount={allCount} />;
}

if (error) {
if (eventsLoadError) {
statusBtnTxt = (
<span className="co-sysevent-stream__connection-error">
{typeof error === 'string'
? t('Error connecting to event stream: {{ error }}', {
error,
})
: t('Error connecting to event stream')}
{t('Error connecting to event stream')}
</span>
);
sysEventStatus = <ErrorLoadingEvents />;
} else if (loading) {
} else if (!eventsLoaded) {
statusBtnTxt = <span>{t('Loading events...')}</span>;
sysEventStatus = <Loading />;
} else if (active) {
Expand Down
2 changes: 0 additions & 2 deletions frontend/public/locales/en/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -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...",
Expand Down