diff --git a/packages/manager/.changeset/pr-11263-tech-stories-1732035738769.md b/packages/manager/.changeset/pr-11263-tech-stories-1732035738769.md
new file mode 100644
index 00000000000..0460f9e406a
--- /dev/null
+++ b/packages/manager/.changeset/pr-11263-tech-stories-1732035738769.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Tech Stories
+---
+
+Optimize Events Polling following changes from incident ([#11263](https://github.com/linode/manager/pull/11263))
diff --git a/packages/manager/src/features/Events/EventsLanding.test.tsx b/packages/manager/src/features/Events/EventsLanding.test.tsx
index 588eccaa406..2f8ec9248fb 100644
--- a/packages/manager/src/features/Events/EventsLanding.test.tsx
+++ b/packages/manager/src/features/Events/EventsLanding.test.tsx
@@ -62,13 +62,20 @@ describe('EventsLanding', () => {
it('renders a message when there are no more events to load', async () => {
const event = eventFactory.build();
+ const eventsResponse = makeResourcePage([event], {
+ page: 1,
+ pages: 1,
+ results: 1,
+ });
server.use(
- http.get('*/events', () =>
- HttpResponse.json(
- makeResourcePage([event], { page: 1, pages: 1, results: 1 })
- )
- )
+ http.get('*/events', () => HttpResponse.json(eventsResponse), {
+ once: true,
+ }),
+ // `useEventsInfiniteQuery` needs to make two fetches to know if there are no more events to show
+ http.get('*/events', () => HttpResponse.json(makeResourcePage([])), {
+ once: true,
+ })
);
const { findByText } = renderWithTheme();
diff --git a/packages/manager/src/features/Events/EventsLanding.tsx b/packages/manager/src/features/Events/EventsLanding.tsx
index e7e69684476..3f221a07516 100644
--- a/packages/manager/src/features/Events/EventsLanding.tsx
+++ b/packages/manager/src/features/Events/EventsLanding.tsx
@@ -44,6 +44,7 @@ export const EventsLanding = (props: Props) => {
events,
fetchNextPage,
hasNextPage,
+ isFetching,
isFetchingNextPage,
isLoading,
} = useEventsInfiniteQuery(filter);
@@ -111,15 +112,13 @@ export const EventsLanding = (props: Props) => {
{renderTableBody()}
- {hasNextPage ? (
+ {!isFetching && hasNextPage && (
fetchNextPage()}>
- ) : (
- events &&
- events.length > 0 && (
- No more events to show
- )
+ )}
+ {events && events.length > 0 && !isFetching && !hasNextPage && (
+ No more events to show
)}
>
);
diff --git a/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx b/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx
index eecf5324b92..ab156458697 100644
--- a/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx
+++ b/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx
@@ -21,7 +21,7 @@ import { usePrevious } from 'src/hooks/usePrevious';
import { useNotificationsQuery } from 'src/queries/account/notifications';
import { isInProgressEvent } from 'src/queries/events/event.helpers';
import {
- useInitialEventsQuery,
+ useEventsInfiniteQuery,
useMarkEventsAsSeen,
} from 'src/queries/events/events';
@@ -34,8 +34,11 @@ export const NotificationMenu = () => {
const formattedNotifications = useFormattedNotifications();
const notificationContext = React.useContext(_notificationContext);
- const { data, events } = useInitialEventsQuery();
- const eventsData = data?.data ?? [];
+ const { data } = useEventsInfiniteQuery();
+
+ // Just use the first page of events because we `slice` to get the first 20 events anyway
+ const events = data?.pages[0].data ?? [];
+
const { mutateAsync: markEventsAsSeen } = useMarkEventsAsSeen();
const numNotifications =
@@ -152,8 +155,8 @@ export const NotificationMenu = () => {
- {eventsData.length > 0 ? (
- eventsData
+ {events.length > 0 ? (
+ events
.slice(0, 20)
.map((event) => (
{
- /**
- * We only want to get events from the last 7 days.
- */
- const [defaultCreatedFilter] = useState(
- DateTime.now()
- .minus({ days: 7 })
- .setZone('utc')
- .toFormat(ISO_DATETIME_NO_TZ_FORMAT)
- );
-
- const query = useQuery, APIError[]>({
- gcTime: Infinity,
- queryFn: () =>
- getEvents(
- {},
- {
- ...EVENTS_LIST_FILTER,
- '+order': 'desc',
- '+order_by': 'id',
- created: { '+gt': defaultCreatedFilter },
- }
- ),
- queryKey: ['events', 'initial'],
- staleTime: Infinity,
- });
-
- const events = query.data?.data;
-
- return { ...query, events };
-};
+const defaultCreatedFilter = DateTime.now()
+ .minus({ days: 7 })
+ .setZone('utc')
+ .toFormat(ISO_DATETIME_NO_TZ_FORMAT);
/**
* Gets an infinitely scrollable list of all Events
@@ -80,25 +44,56 @@ export const useInitialEventsQuery = () => {
* the next set of events when the items returned by the server may have shifted.
*/
export const useEventsInfiniteQuery = (filter: Filter = EVENTS_LIST_FILTER) => {
+ const queryClient = useQueryClient();
+
const query = useInfiniteQuery, APIError[]>({
gcTime: Infinity,
- getNextPageParam: ({ data, results }) => {
- if (results === data.length) {
- return undefined;
+ getNextPageParam: (lastPage, allPages) => {
+ if (allPages.length === 1 && lastPage.results === 0) {
+ // If we did the inital fetch (the one that limits results to 7 days) but got no results,
+ // we can't conclude there are no more pages to fetch. There could be more events to fetch
+ // outside of the 7 day window. Therefore, we return a "fake" pageParam so that React Query
+ // will still attempt to fetch another page whenever `fetchNextPage` is called next.
+ return 'fetch more';
}
- return data[data.length - 1].id;
+ return lastPage.data[lastPage.data.length - 1]?.id;
},
initialPageParam: undefined,
- queryFn: ({ pageParam }) =>
- getEvents(
+ queryFn: ({ pageParam }) => {
+ const data = queryClient.getQueryData>>([
+ 'events',
+ 'infinite',
+ filter,
+ ]);
+ if (data === undefined) {
+ // If there is no data in the cache yet at this query key,
+ // it likely means that this is the initial fetch. For the
+ // initial fetch, we have been asked to use `created` to limit
+ // the timeframe for our initial fetch to a small window.
+ // See M3-8450 for context.
+ return getEvents(
+ {},
+ {
+ ...filter,
+ '+order': 'desc',
+ '+order_by': 'id',
+ created: { '+gt': defaultCreatedFilter },
+ }
+ );
+ }
+ return getEvents(
{},
{
...filter,
'+order': 'desc',
'+order_by': 'id',
- id: pageParam ? { '+lt': pageParam } : undefined,
+ id:
+ pageParam === 'fetch more'
+ ? undefined
+ : { '+lt': pageParam as number },
}
- ),
+ );
+ },
queryKey: ['events', 'infinite', filter],
staleTime: Infinity,
});
@@ -148,9 +143,9 @@ export const useEventsPoller = () => {
const queryClient = useQueryClient();
- const { data: initialEvents } = useInitialEventsQuery();
+ const { data } = useEventsInfiniteQuery();
- const hasFetchedInitialEvents = initialEvents !== undefined;
+ const hasFetchedInitialEvents = data !== undefined;
const [mountTimestamp] = useState(
DateTime.now().setZone('utc').toFormat(ISO_DATETIME_NO_TZ_FORMAT)
@@ -159,11 +154,15 @@ export const useEventsPoller = () => {
const { data: events } = useQuery({
enabled: hasFetchedInitialEvents,
queryFn: () => {
- const data = queryClient.getQueryData>([
+ const data = queryClient.getQueryData>>([
'events',
- 'initial',
+ 'infinite',
+ EVENTS_LIST_FILTER,
]);
- const events = data?.data;
+ const events = data?.pages.reduce(
+ (events, page) => [...events, ...page.data],
+ []
+ );
// If the user has events, poll for new events based on the most recent event's created time.
// If the user has no events, poll events from the time the app mounted.
@@ -242,26 +241,8 @@ export const useMarkEventsAsSeen = () => {
const queryClient = useQueryClient();
return useMutation<{}, APIError[], number>({
- mutationFn: (eventId) => markEventSeen(eventId),
+ mutationFn: markEventSeen,
onSuccess: (_, eventId) => {
- // Update Initial Query
- queryClient.setQueryData>(
- ['events', 'initial'],
- (prev) => {
- if (!prev) {
- return undefined;
- }
-
- for (const event of prev.data) {
- if (event.id <= eventId) {
- event.seen = true;
- }
- }
-
- return prev;
- }
- );
-
// Update Infinite Queries
queryClient.setQueriesData>>(
{ queryKey: ['events', 'infinite'] },
@@ -319,8 +300,6 @@ export const updateEventsQueries = (
updateEventsQuery(filteredEvents, queryKey, queryClient);
});
-
- updateInitialEventsQuery(events, queryClient);
};
/**
@@ -385,44 +364,3 @@ export const updateEventsQuery = (
}
);
};
-
-export const updateInitialEventsQuery = (
- events: Event[],
- queryClient: QueryClient
-) => {
- queryClient.setQueryData>(
- ['events', 'initial'],
- (prev) => {
- if (!prev) {
- return undefined;
- }
- const updatedEventIndexes: number[] = [];
-
- for (let i = 0; i < events.length; i++) {
- const indexOfEvent = prev.data.findIndex((e) => e.id === events[i].id);
-
- if (indexOfEvent !== -1) {
- prev.data[indexOfEvent] = events[i];
- updatedEventIndexes.push(i);
- }
- }
-
- const newEvents: Event[] = [];
-
- for (let i = 0; i < events.length; i++) {
- if (!updatedEventIndexes.includes(i)) {
- newEvents.push(events[i]);
- }
- }
-
- if (newEvents.length > 0) {
- // For all events, that remain, append them to the top of the events list
- prev.data = [...newEvents, ...prev.data];
-
- prev.results += newEvents.length;
- }
-
- return prev;
- }
- );
-};