Skip to content

Commit 812aa28

Browse files
committed
Refactor EventStream
1 parent b6b1302 commit 812aa28

2 files changed

Lines changed: 121 additions & 129 deletions

File tree

frontend/public/components/events.jsx

Lines changed: 115 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as _ from 'lodash-es';
33
import * as React from 'react';
44
import * as classNames from 'classnames';
55
import * as PropTypes from 'prop-types';
6-
import { useParams, Link } from 'react-router-dom-v5-compat';
6+
import { Link, useParams } from 'react-router-dom-v5-compat';
77
import { Helmet } from 'react-helmet';
88
import { Chip, ChipGroup } from '@patternfly/react-core';
99
import { Trans, useTranslation } from 'react-i18next';
@@ -61,7 +61,7 @@ export const typeFilter = (eventType, event) => {
6161
};
6262

6363
const kindFilter = (reference, { involvedObject }) => {
64-
if (reference === 'all') {
64+
if (!reference) {
6565
return true;
6666
}
6767
const kinds = reference.split(',');
@@ -179,29 +179,28 @@ export const EventsList = (props) => {
179179
const { t } = useTranslation();
180180
const [type, setType] = React.useState('all');
181181
const [textFilter, setTextFilter] = React.useState('');
182-
const resourceTypeAll = 'all';
183182
const { ns } = useParams();
184-
const [selected, setSelected] = React.useState(new Set([resourceTypeAll]));
183+
const [selected, setSelected] = React.useState(new Set([]));
185184
const eventTypes = {
186185
all: t('public~All types'),
187186
normal: t('public~Normal'),
188187
warning: t('public~Warning'),
189188
};
190189

191190
const toggleSelected = (selection) => {
192-
if (selected.has(resourceTypeAll) || selection === resourceTypeAll) {
193-
setSelected(new Set([selection]));
194-
} else {
195-
const updateItems = new Set(selected);
191+
setSelected((prev) => {
192+
const updateItems = new Set(prev);
196193
updateItems.has(selection) ? updateItems.delete(selection) : updateItems.add(selection);
197-
setSelected(updateItems);
198-
}
194+
return updateItems;
195+
});
199196
};
200197

201198
const removeResource = (selection) => {
202-
const updateItems = new Set(selected);
203-
updateItems.delete(selection);
204-
setSelected(updateItems);
199+
setSelected((prev) => {
200+
const updateItems = new Set(prev);
201+
updateItems.delete(selection);
202+
return updateItems;
203+
});
205204
};
206205

207206
const clearSelection = () => {
@@ -241,11 +240,10 @@ export const EventsList = (props) => {
241240
expandedText={t('public~Show less')}
242241
>
243242
{[...selected].map((chip) => {
244-
const chipString = chip === resourceTypeAll ? t('public~All') : chip;
245243
return (
246244
<Chip key={chip} onClick={() => removeResource(chip)}>
247-
<ResourceIcon kind={chipString} />
248-
{kindForReference(chipString)}
245+
<ResourceIcon kind={chip} />
246+
{kindForReference(chip)}
249247
</Chip>
250248
);
251249
})}
@@ -259,11 +257,7 @@ export const EventsList = (props) => {
259257
namespace={ns}
260258
key={[...selected].join(',')}
261259
type={type}
262-
kind={
263-
selected.has(resourceTypeAll) || selected.size === 0
264-
? resourceTypeAll
265-
: [...selected].join(',')
266-
}
260+
kind={[...selected].join(',')}
267261
mock={props.mock}
268262
textFilter={textFilter}
269263
/>
@@ -289,8 +283,12 @@ export const NoMatchingEvents = ({ allCount }) => {
289283
<div className="cos-status-box__title">{t('public~No matching events')}</div>
290284
<div className="pf-v5-u-text-align-center cos-status-box__detail">
291285
{allCount >= maxMessages
292-
? t('public~{{allCount}}+ events exist, but none match the current filter', { allCount })
293-
: t('public~{{allCount}} events exist, but none match the current filter', { allCount })}
286+
? t('public~{{count}}+ event exist, but none match the current filter', {
287+
count: maxMessages,
288+
})
289+
: t('public~{{count}} event exist, but none match the current filter', {
290+
count: allCount,
291+
})}
294292
</div>
295293
</Box>
296294
);
@@ -328,119 +326,112 @@ export const EventStreamPage = withStartGuide(({ noProjectsAvailable, ...rest })
328326
);
329327
});
330328

331-
const EventStream = (props) => {
329+
const EventStream = ({
330+
namespace,
331+
fieldSelector,
332+
mock,
333+
resourceEventStream,
334+
kind,
335+
type,
336+
filter,
337+
textFilter,
338+
}) => {
339+
const { t } = useTranslation();
332340
const [active, setActive] = React.useState(true);
333-
const [sortedMessages, setSortedMessages] = React.useState([]);
334-
const [filteredEvents, setFilteredEvents] = React.useState([]);
341+
const [sortedEvents, setSortedEvents] = React.useState([]);
335342
const [error, setError] = React.useState(null);
336343
const [loading, setLoading] = React.useState(true);
344+
const ws = React.useRef(null);
337345

338-
const { t } = useTranslation();
339-
340-
const { fieldSelector, mock, resourceEventStream } = props;
341-
342-
let messages = {};
343-
let ws;
346+
const filteredEvents = React.useMemo(() => {
347+
return filterEvents(sortedEvents, { kind, type, filter, textFilter }).slice(0, maxMessages);
348+
}, [sortedEvents, kind, type, filter, textFilter]);
344349

345-
const wsInit = (ns) => {
346-
const params = { ns };
347-
if (fieldSelector) {
348-
params.queryParams = { fieldSelector: encodeURIComponent(fieldSelector) };
349-
}
350+
// Handle websocket setup and teardown when dependent props change
351+
React.useEffect(() => {
352+
ws.current?.destroy();
353+
if (!mock) {
354+
const webSocketID = `${namespace || 'all'}-sysevents`;
355+
const watchURLOptions = {
356+
...(namespace ? { ns: namespace } : {}),
357+
...(fieldSelector
358+
? {
359+
queryParams: {
360+
fieldSelector: encodeURIComponent(fieldSelector),
361+
},
362+
}
363+
: {}),
364+
};
365+
const path = watchURL(EventModel, watchURLOptions);
366+
const webSocketOptions = {
367+
host: 'auto',
368+
reconnect: true,
369+
path,
370+
jsonParse: true,
371+
bufferFlushInterval: flushInterval,
372+
bufferMax: maxMessages,
373+
};
350374

351-
ws = new WSFactory(`${ns || 'all'}-sysevents`, {
352-
host: 'auto',
353-
reconnect: true,
354-
path: watchURL(EventModel, params),
355-
jsonParse: true,
356-
bufferFlushInterval: flushInterval,
357-
bufferMax: maxMessages,
358-
})
359-
.onbulkmessage((events) => {
360-
events.forEach(({ object, type }) => {
361-
const uid = object.metadata.uid;
362-
363-
switch (type) {
364-
case 'ADDED':
365-
case 'MODIFIED':
366-
if (messages[uid] && messages[uid].count > object.count) {
367-
// We already have a more recent version of this message stored, so skip this one
368-
return;
375+
ws.current = new WSFactory(webSocketID, webSocketOptions)
376+
.onbulkmessage((messages) => {
377+
// Make one update to state per batch of events.
378+
setSortedEvents((currentSortedEvents) => {
379+
const topEvents = currentSortedEvents.slice(0, maxMessages);
380+
const batch = messages.reduce((acc, { object, type: eventType }) => {
381+
const uid = object.metadata.uid;
382+
switch (eventType) {
383+
case 'ADDED':
384+
case 'MODIFIED':
385+
if (acc[uid] && acc[uid].count > object.count) {
386+
// We already have a more recent version of this message stored, so skip this one
387+
return acc;
388+
}
389+
return { ...acc, [uid]: object };
390+
case 'DELETED':
391+
return _.omit(acc, uid);
392+
default:
393+
// eslint-disable-next-line no-console
394+
console.error(`UNHANDLED EVENT: ${eventType}`);
395+
return acc;
369396
}
370-
messages[uid] = object;
371-
break;
372-
case 'DELETED':
373-
delete messages[uid];
374-
break;
375-
default:
376-
// eslint-disable-next-line no-console
377-
console.error(`UNHANDLED EVENT: ${type}`);
378-
return;
397+
}, _.keyBy(topEvents, 'metadata.uid'));
398+
return sortEvents(batch);
399+
});
400+
})
401+
.onopen(() => {
402+
setError(false);
403+
setLoading(false);
404+
})
405+
.onclose((evt) => {
406+
if (evt?.wasClean === false) {
407+
setError(evt.reason || t('public~Connection did not close cleanly.'));
379408
}
409+
})
410+
.onerror(() => {
411+
setError(true);
380412
});
381-
flushMessages();
382-
})
383-
.onopen(() => {
384-
setError(false);
385-
setLoading(false);
386-
})
387-
.onclose((evt) => {
388-
if (evt && evt.wasClean === false) {
389-
setError(evt.reason || t('public~Connection did not close cleanly.'));
390-
}
391-
})
392-
.onerror(() => {
393-
setError(true);
394-
});
395-
};
413+
}
414+
return () => {
415+
ws.current?.destroy();
416+
};
417+
}, [namespace, fieldSelector, mock, t]);
396418

419+
// Pause/unpause the websocket when the active state changes
397420
React.useEffect(() => {
398-
// If the namespace has changed, create a new WebSocket with the new namespace
399-
if (!props.mock) {
400-
wsInit(props.namespace);
401-
// Reset the messages and events
402-
setSortedMessages([]);
403-
setFilteredEvents([]);
404-
405-
return () => {
406-
ws && ws.destroy();
407-
};
421+
if (active) {
422+
ws.current?.unpause();
423+
} else {
424+
ws.current?.pause();
408425
}
409-
// eslint-disable-next-line react-hooks/exhaustive-deps
410-
}, [props.namespace]);
411-
412-
const prevSortedMessages = React.useRef([]);
426+
}, [active]);
413427

414428
const toggleStream = () => {
415429
setActive((prev) => !prev);
416-
prevSortedMessages.current = sortedMessages;
417-
};
418-
419-
React.useEffect(() => {
420-
// If the filter has changed, update the filteredEvents
421-
setFilteredEvents(
422-
EventStream.filterEvents(active ? sortedMessages : prevSortedMessages.current, props),
423-
);
424-
}, [active, sortedMessages, props, prevSortedMessages]);
425-
426-
// Messages can come in extremely fast when the buffer flushes.
427-
// Instead of calling setState() on every single message, let onmessage()
428-
// update an instance variable, and throttle the actual UI update (see constructor)
429-
const flushMessages = () => {
430-
const sorted = sortEvents(messages);
431-
sorted.splice(maxMessages);
432-
setSortedMessages(sorted);
433-
setFilteredEvents(
434-
EventStream.filterEvents(active ? sorted : prevSortedMessages.current, props),
435-
);
436-
437-
// Shrink messages back to maxMessages messages, to stop it growing indefinitely
438-
messages = _.keyBy(sorted, 'metadata.uid');
439430
};
440431

441432
const count = filteredEvents.length;
442-
const allCount = sortedMessages.length;
443-
const noEvents = allCount === 0 && ws && ws.bufferSize() === 0;
433+
const allCount = sortedEvents.length;
434+
const noEvents = allCount === 0;
444435
const noMatches = allCount > 0 && count === 0;
445436
let sysEventStatus, statusBtnTxt;
446437

@@ -455,7 +446,9 @@ const EventStream = (props) => {
455446
statusBtnTxt = (
456447
<span className="co-sysevent-stream__connection-error">
457448
{_.isString(error)
458-
? t('public~Error connecting to event stream: { error }', { error })
449+
? t('public~Error connecting to event stream: { error }', {
450+
error,
451+
})
459452
: t('public~Error connecting to event stream')}
460453
</span>
461454
);
@@ -475,10 +468,7 @@ const EventStream = (props) => {
475468
const messageCount =
476469
count < maxMessages
477470
? t('public~Showing {{count}} event', { count })
478-
: t('public~Showing {{messageCount}} of {{allCount}}+ events', {
479-
messageCount: count,
480-
allCount,
481-
});
471+
: t('public~Showing most recent {{count}} event', { count });
482472

483473
return (
484474
<div className="co-m-pane__body">
@@ -509,7 +499,7 @@ const EventStream = (props) => {
509499

510500
EventStream.defaultProps = {
511501
type: 'all',
512-
kind: 'all',
502+
kind: '',
513503
mock: false,
514504
};
515505

@@ -523,7 +513,7 @@ EventStream.propTypes = {
523513
textFilter: PropTypes.string,
524514
};
525515

526-
EventStream.filterEvents = (messages, { kind, type, filter, textFilter }) => {
516+
const filterEvents = (messages, { kind, type, filter, textFilter }) => {
527517
// Don't use `fuzzy` because it results in some surprising matches in long event messages.
528518
// Instead perform an exact substring match on each word in the text filter.
529519
const words = _.uniq(_.toLower(textFilter).match(/\S+/g)).sort((a, b) => {

frontend/public/locales/en/public.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -545,11 +545,12 @@
545545
"Events by name or message": "Events by name or message",
546546
"{{numRemaining}} more": "{{numRemaining}} more",
547547
"Show less": "Show less",
548-
"All": "All",
549548
"No events": "No events",
550549
"No matching events": "No matching events",
551-
"{{allCount}}+ events exist, but none match the current filter": "{{allCount}}+ events exist, but none match the current filter",
552-
"{{allCount}} events exist, but none match the current filter": "{{allCount}} events exist, but none match the current filter",
550+
"{{count}}+ event exist, but none match the current filter_one": "{{count}}+ event exist, but none match the current filter",
551+
"{{count}}+ event exist, but none match the current filter_other": "{{count}}+ event exist, but none match the current filters",
552+
"{{count}} event exist, but none match the current filter_one": "{{count}} event exist, but none match the current filter",
553+
"{{count}} event exist, but none match the current filter_other": "{{count}} event exist, but none match the current filters",
553554
"Error loading events": "Error loading events",
554555
"An error occurred during event retrieval. Attempting to reconnect...": "An error occurred during event retrieval. Attempting to reconnect...",
555556
"Events": "Events",
@@ -561,7 +562,8 @@
561562
"Event stream is paused.": "Event stream is paused.",
562563
"Showing {{count}} event_one": "Showing {{count}} event",
563564
"Showing {{count}} event_other": "Showing {{count}} events",
564-
"Showing {{messageCount}} of {{allCount}}+ events": "Showing {{messageCount}} of {{allCount}}+ events",
565+
"Showing most recent {{count}} event_one": "Showing most recent {{count}} event",
566+
"Showing most recent {{count}} event_other": "Showing most recent {{count}} events",
565567
"Older events are not stored.": "Older events are not stored.",
566568
"Filter {{label}}...": "Filter {{label}}...",
567569
"Create {{label}}": "Create {{label}}",

0 commit comments

Comments
 (0)