From eeb1b48ffbd0393f624dd98dff512071d4cdbf54 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 13:25:26 -0500 Subject: [PATCH 01/44] events landing --- .../src/features/Events/EventsLanding.tsx | 264 ++---------------- packages/manager/src/queries/events.ts | 20 ++ 2 files changed, 39 insertions(+), 245 deletions(-) create mode 100644 packages/manager/src/queries/events.ts diff --git a/packages/manager/src/features/Events/EventsLanding.tsx b/packages/manager/src/features/Events/EventsLanding.tsx index 9e181ca0020..2637cb4025b 100644 --- a/packages/manager/src/features/Events/EventsLanding.tsx +++ b/packages/manager/src/features/Events/EventsLanding.tsx @@ -1,11 +1,6 @@ import { Event, getEvents } from '@linode/api-v4/lib/account'; -import { ResourcePage } from '@linode/api-v4/lib/types'; -import { useSnackbar } from 'notistack'; -import { concat, compose as rCompose, uniq } from 'ramda'; import * as React from 'react'; -import { connect } from 'react-redux'; import { Waypoint } from 'react-waypoint'; -import { compose } from 'recompose'; import { Hidden } from 'src/components/Hidden'; import { Table } from 'src/components/Table'; @@ -16,12 +11,8 @@ import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import { ApplicationState } from 'src/store'; -import { setDeletedEvents } from 'src/store/events/event.helpers'; -import { ExtendedEvent } from 'src/store/events/event.types'; -import { removeBlocklistedEvents } from 'src/utilities/eventUtils'; +import { useEventsInfiniteQuery } from 'src/queries/events'; -import { filterUniqueEvents, shouldUpdateEvents } from './Event.helpers'; import { EventRow } from './EventRow'; import { StyledH1Header, @@ -38,213 +29,15 @@ interface Props { getEventsRequest?: typeof getEvents; } -type CombinedProps = Props & StateProps; - -const appendToEvents = (oldEvents: Event[], newEvents: Event[]) => - rCompose( - uniq, // Ensure no duplicates - concat(oldEvents), // Attach the new events - setDeletedEvents // Add a _deleted entry for each new event - )(newEvents); - -export interface ReducerState { - eventsFromRedux: ExtendedEvent[]; - inProgressEvents: Record; - mostRecentEventTime: string; - reactStateEvents: Event[]; -} - -interface Payload { - entityId?: number; - eventsFromRedux: ExtendedEvent[]; - inProgressEvents: Record; - mostRecentEventTime: string; - reactStateEvents: Event[]; -} - -export interface ReducerActions { - payload: Payload; - type: 'append' | 'prepend'; -} - -type EventsReducer = React.Reducer; - -export const reducer: EventsReducer = (state, action) => { +export const EventsLanding = (props: Props) => { const { - payload: { - entityId, - eventsFromRedux: nextReduxEvents, - inProgressEvents: nextInProgressEvents, - mostRecentEventTime: nextMostRecentEventTime, - reactStateEvents: nextReactEvents, - }, - } = action; - - switch (action.type) { - case 'prepend': - if ( - shouldUpdateEvents( - { - inProgressEvents: state.inProgressEvents, - mostRecentEventTime: state.mostRecentEventTime, - }, - { - inProgressEvents: nextInProgressEvents, - mostRecentEventTime: nextMostRecentEventTime, - } - ) - ) { - return { - eventsFromRedux: nextReduxEvents, - inProgressEvents: nextInProgressEvents, - mostRecentEventTime: nextMostRecentEventTime, - reactStateEvents: filterUniqueEvents([ - /* - Pop new events from Redux on the top of the event stream, with some conditions - */ - ...nextReduxEvents.filter((eachEvent) => { - return ( - /** all events from Redux will have this flag as a boolean value */ - !eachEvent._initial && - /** - * so here we're basically determining whether or not - * an entityID prop was passed, and if so, only show the events - * that pertain to that entity. This is useful because it helps - * us show only relevant events on the Linode Activity panel, for example - */ - (typeof entityId === 'undefined' || - (eachEvent.entity && eachEvent.entity.id === entityId)) - ); - }), - /* - at this point, the state is populated with events from the cDM - request (which don't include the "_initial flag"), but it might also - contain events from Redux as well. We only want the ones where the "_initial" - flag doesn't exist - */ - ...nextReactEvents.filter( - (eachEvent) => typeof eachEvent._initial === 'undefined' - ), - ]), - }; - } - return { - eventsFromRedux: nextReduxEvents, - inProgressEvents: nextInProgressEvents, - mostRecentEventTime: nextMostRecentEventTime, - reactStateEvents: nextReactEvents, - }; - case 'append': - default: - return { - eventsFromRedux: nextReduxEvents, - inProgressEvents: nextInProgressEvents, - mostRecentEventTime: nextMostRecentEventTime, - reactStateEvents: appendToEvents( - state.reactStateEvents, - nextReactEvents - ), - }; - } -}; - -export const EventsLanding = (props: CombinedProps) => { - const { enqueueSnackbar } = useSnackbar(); - - const [loading, setLoading] = React.useState(false); - const [loadMoreEvents, setLoadMoreEvents] = React.useState(false); - const [error, setError] = React.useState(undefined); - const [currentPage, setCurrentPage] = React.useState(1); - const [isRequesting, setRequesting] = React.useState(false); - const [initialLoaded, setInitialLoaded] = React.useState(false); - - const [events, dispatch] = React.useReducer(reducer, { - eventsFromRedux: props.eventsFromRedux, - inProgressEvents: props.inProgressEvents, - mostRecentEventTime: props.mostRecentEventTime, - reactStateEvents: [], - }); - - const getNext = () => { - if (isRequesting) { - return; - } - setRequesting(true); - - const getEventsRequest = props.getEventsRequest || getEvents; - - getEventsRequest({ page: currentPage }) - .then(handleEventsRequestSuccess) - .catch(() => { - enqueueSnackbar('There was an error loading more events', { - variant: 'error', - }); - setLoading(false); - setRequesting(false); - }); - }; - - const handleEventsRequestSuccess = (response: ResourcePage) => { - setCurrentPage(currentPage + 1); - setLoadMoreEvents(true); - /** append our events to component state */ - dispatch({ - payload: { - entityId: props.entityId, - eventsFromRedux: props.eventsFromRedux, - inProgressEvents: props.inProgressEvents, - mostRecentEventTime: props.mostRecentEventTime, - reactStateEvents: response.data, - }, - type: 'append', - }); - setLoading(false); - setRequesting(false); - setError(undefined); - if (response.pages === currentPage) { - setLoadMoreEvents(false); - } - }; - - React.useEffect(() => { - setLoading(true); - setRequesting(true); - setError(undefined); - - const getEventsRequest = props.getEventsRequest || getEvents; - - getEventsRequest() - .then(handleEventsRequestSuccess) - .then(() => setInitialLoaded(true)) - .catch(() => { - setLoading(false); - setError('Error'); - }); - }, []); - - /** - * For the purposes of concat-ing the events from Redux and React state - * so we can display events in real-time - */ - React.useEffect(() => { - const { eventsFromRedux, inProgressEvents } = props; - /** in this case, we're getting new events from Redux, so we want to prepend */ - dispatch({ - payload: { - entityId: props.entityId, - eventsFromRedux, - inProgressEvents, - mostRecentEventTime: props.mostRecentEventTime, - reactStateEvents: events.reactStateEvents, - }, - type: 'prepend', - }); - }, [ - events.reactStateEvents, - props, - props.eventsFromRedux, - props.inProgressEvents, - ]); + events, + isLoading, + hasNextPage, + fetchNextPage, + error, + isFetchingNextPage + } = useEventsInfiniteQuery(); const { emptyMessage, entityId, errorMessage } = props; @@ -271,24 +64,22 @@ export const EventsLanding = (props: CombinedProps) => { {renderTableBody( - loading, - isRequesting, + isLoading, + isFetchingNextPage, errorMessage, entityId, - error, - events.reactStateEvents, + error?.[0].reason, + events, emptyMessage )} - {loadMoreEvents && initialLoaded && !loading ? ( - + {hasNextPage ? ( + fetchNextPage()}>
) : ( - !loading && - !error && - events.reactStateEvents.length > 0 && ( + events && events.length > 0 && ( No more events to show ) )} @@ -305,8 +96,6 @@ export const renderTableBody = ( events?: Event[], emptyMessage = "You don't have any events on your account." ) => { - const filteredEvents = removeBlocklistedEvents(events, ['profile_update']); - if (loading) { return ( ); - } else if (filteredEvents.length === 0) { + } else if (events && events.length === 0) { return ( - {filteredEvents.map((thisEvent, idx) => ( + {events?.map((thisEvent, idx) => ( ; - mostRecentEventTime: string; -} - -const mapStateToProps = (state: ApplicationState) => ({ - eventsFromRedux: state.events.events, - inProgressEvents: state.events.inProgressEvents, - mostRecentEventTime: state.events.mostRecentEventTime, -}); - -const connected = connect(mapStateToProps); - -const enhanced = compose(connected); -export default enhanced(EventsLanding); +export default EventsLanding; diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts new file mode 100644 index 00000000000..b69e2640da9 --- /dev/null +++ b/packages/manager/src/queries/events.ts @@ -0,0 +1,20 @@ +import { APIError, Event, ResourcePage, getEvents } from '@linode/api-v4'; +import { useInfiniteQuery } from 'react-query'; + +export const useEventsInfiniteQuery = () => { + const query = useInfiniteQuery, APIError[]>( + ['events', 'infinite'], + ({ pageParam }) => getEvents({ page: pageParam }), + { + getNextPageParam: ({ page, pages }) => + page < pages ? page + 1 : undefined, + } + ); + + const events = query.data?.pages.reduce( + (events, page) => [...events, ...page.data], + [] + ); + + return { ...query, events }; +}; From 66ab52f71c6122d9bb0a97a64218f7ac404be850 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 13:39:23 -0500 Subject: [PATCH 02/44] add linode events support with filter --- .../src/features/Events/EventsLanding.tsx | 39 ++++++++++--------- packages/manager/src/queries/events.ts | 10 +++-- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/manager/src/features/Events/EventsLanding.tsx b/packages/manager/src/features/Events/EventsLanding.tsx index 2637cb4025b..96751619454 100644 --- a/packages/manager/src/features/Events/EventsLanding.tsx +++ b/packages/manager/src/features/Events/EventsLanding.tsx @@ -1,4 +1,4 @@ -import { Event, getEvents } from '@linode/api-v4/lib/account'; +import { Event } from '@linode/api-v4/lib/account'; import * as React from 'react'; import { Waypoint } from 'react-waypoint'; @@ -21,25 +21,31 @@ import { StyledTypography, } from './EventsLanding.styles'; +import type { Filter } from '@linode/api-v4'; + interface Props { emptyMessage?: string; // Custom message for the empty state (i.e. no events). - // isEventsLandingForEntity?: boolean; entityId?: number; - errorMessage?: string; // Custom error message (for an entity's Activity page, for example) - getEventsRequest?: typeof getEvents; } export const EventsLanding = (props: Props) => { + const { emptyMessage, entityId } = props; + + const filter: Filter = { action: { '+neq': 'profile_update' } }; + + if (entityId) { + filter['entity.id'] = entityId; + filter['entity.type'] = 'linode'; + } + const { + error, events, - isLoading, - hasNextPage, fetchNextPage, - error, - isFetchingNextPage - } = useEventsInfiniteQuery(); - - const { emptyMessage, entityId, errorMessage } = props; + hasNextPage, + isFetchingNextPage, + isLoading, + } = useEventsInfiniteQuery(filter); return ( <> @@ -66,7 +72,6 @@ export const EventsLanding = (props: Props) => { {renderTableBody( isLoading, isFetchingNextPage, - errorMessage, entityId, error?.[0].reason, events, @@ -79,7 +84,8 @@ export const EventsLanding = (props: Props) => {
) : ( - events && events.length > 0 && ( + events && + events.length > 0 && ( No more events to show ) )} @@ -90,7 +96,6 @@ export const EventsLanding = (props: Props) => { export const renderTableBody = ( loading: boolean, isRequesting: boolean, - errorMessage = 'There was an error retrieving the events on your account.', entityId?: number, error?: string, events?: Event[], @@ -106,11 +111,7 @@ export const renderTableBody = ( ); } else if (error) { return ( - + ); } else if (events && events.length === 0) { return ( diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index b69e2640da9..0f641489416 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -1,10 +1,12 @@ -import { APIError, Event, ResourcePage, getEvents } from '@linode/api-v4'; +import { getEvents } from '@linode/api-v4'; import { useInfiniteQuery } from 'react-query'; -export const useEventsInfiniteQuery = () => { +import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; + +export const useEventsInfiniteQuery = (filter: Filter = {}) => { const query = useInfiniteQuery, APIError[]>( - ['events', 'infinite'], - ({ pageParam }) => getEvents({ page: pageParam }), + ['events', 'infinite', filter], + ({ pageParam }) => getEvents({ page: pageParam }, filter), { getNextPageParam: ({ page, pages }) => page < pages ? page + 1 : undefined, From 5a36fcd9c7c705d8a23575562afef318e967513e Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 14:28:08 -0500 Subject: [PATCH 03/44] clean up --- .../src/features/Events/EventsLanding.test.ts | 109 ------------------ .../LinodeActivity/LinodeActivity.tsx | 6 - .../src/utilities/getEventsForEntity.ts | 14 --- 3 files changed, 129 deletions(-) delete mode 100644 packages/manager/src/features/Events/EventsLanding.test.ts delete mode 100644 packages/manager/src/utilities/getEventsForEntity.ts diff --git a/packages/manager/src/features/Events/EventsLanding.test.ts b/packages/manager/src/features/Events/EventsLanding.test.ts deleted file mode 100644 index 94d7674b6a0..00000000000 --- a/packages/manager/src/features/Events/EventsLanding.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Event } from '@linode/api-v4/lib/account'; - -import { reduxEvent, uniqueEvents } from 'src/__data__/events'; - -import { ReducerActions, ReducerState, reducer } from './EventsLanding'; - -const someEvent: Event[] = [ - { - action: 'linode_boot', - created: '2018-12-02T20:23:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 1234, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, -]; - -const currentState: ReducerState = { - eventsFromRedux: [], - inProgressEvents: { 123: 50 }, - mostRecentEventTime: 'hello world', - reactStateEvents: uniqueEvents, -}; - -const appendPayload: ReducerActions = { - payload: currentState, - type: 'append', -}; - -const prependPayloadNoChange: ReducerActions = { - payload: currentState, - type: 'prepend', -}; - -describe('utility functions', () => { - it(`should append new events and filter out duplicates to the list of events - when the "append" type is invoked`, () => { - expect(reducer(currentState, appendPayload)).toEqual(appendPayload.payload); - expect( - reducer(currentState, { - ...appendPayload, - payload: { - ...appendPayload.payload, - reactStateEvents: [...someEvent, ...someEvent], - }, - }) - ).toEqual({ - ...currentState, - reactStateEvents: [...uniqueEvents, ...someEvent], - }); - expect(reducer(currentState, appendPayload)).not.toEqual({ - ...currentState, - reactStateEvents: [...uniqueEvents, ...uniqueEvents], - }); - }); - - it('should prepend new events to the list of events and filter out duplicates when the "prepend" type is invoked', () => { - expect( - reducer(currentState, { - ...prependPayloadNoChange, - payload: { - ...prependPayloadNoChange.payload, - eventsFromRedux: [reduxEvent], - mostRecentEventTime: 'ahhhhhhhh', - }, - }) - ).toEqual({ - eventsFromRedux: [reduxEvent], - inProgressEvents: { - 123: 50, - }, - mostRecentEventTime: 'ahhhhhhhh', - reactStateEvents: [reduxEvent, ...uniqueEvents], - }); - - expect( - reducer(currentState, { - ...prependPayloadNoChange, - payload: { - ...prependPayloadNoChange.payload, - eventsFromRedux: [reduxEvent], - inProgressEvents: { 123: 70 }, - }, - }) - ).toEqual({ - eventsFromRedux: [reduxEvent], - inProgressEvents: { 123: 70 }, - mostRecentEventTime: 'hello world', - reactStateEvents: [reduxEvent, ...uniqueEvents], - }); - }); - - it('should not prepend when inProgress and mostRecentEventTime has changed', () => { - expect(reducer(currentState, prependPayloadNoChange)).toEqual(currentState); - }); -}); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.tsx index 73e64d06508..9ea3ec4ec59 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.tsx @@ -1,9 +1,7 @@ -import { Params } from '@linode/api-v4'; import * as React from 'react'; import { useParams } from 'react-router-dom'; import EventsLanding from 'src/features/Events/EventsLanding'; -import { getEventsForEntity } from 'src/utilities/getEventsForEntity'; const LinodeActivity = () => { const { linodeId } = useParams<{ linodeId: string }>(); @@ -11,13 +9,9 @@ const LinodeActivity = () => { return ( - getEventsForEntity(params, 'linode', id) - } data-testid="linode-events-table" emptyMessage="No recent activity for this Linode." entityId={id} - errorMessage="There was an error retrieving activity for this Linode." /> ); }; diff --git a/packages/manager/src/utilities/getEventsForEntity.ts b/packages/manager/src/utilities/getEventsForEntity.ts deleted file mode 100644 index 1e3752bd087..00000000000 --- a/packages/manager/src/utilities/getEventsForEntity.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Params } from '@linode/api-v4'; -import { getEvents } from '@linode/api-v4/lib/account'; - -type EntityTypes = 'domain' | 'linode' | 'nodebalancer' | 'volume'; - -export const getEventsForEntity = ( - params: Params = {}, - entityType: EntityTypes, - entityId: number -) => - getEvents(params, { - 'entity.id': entityId, - 'entity.type': entityType, - }); From 1ea1623e593b545809d4c2e906871690d53b9b5c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 16:00:31 -0500 Subject: [PATCH 04/44] add poller --- packages/manager/src/App.tsx | 2 + .../manager/src/hooks/useInitialRequests.ts | 2 +- packages/manager/src/queries/events.ts | 60 ++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index 6a2d738828e..688ec0e1ac0 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -20,6 +20,7 @@ import { useInitialRequests } from './hooks/useInitialRequests'; import { useNewRelic } from './hooks/useNewRelic'; import { useToastNotifications } from './hooks/useToastNotifications'; import { useSetupFeatureFlags } from './useSetupFeatureFlags'; +import { useEventsPoller } from './queries/events'; // Ensure component's display name is 'App' export const App = () => ; @@ -34,6 +35,7 @@ const BaseApp = withDocumentTitleProvider( const { goToOpen, setGoToOpen } = useGlobalKeyboardListener(); useEventHandlers(); + useEventsPoller(); useToastNotifications(); useAdobeAnalytics(); diff --git a/packages/manager/src/hooks/useInitialRequests.ts b/packages/manager/src/hooks/useInitialRequests.ts index c2bc651c79e..00b5205ecc6 100644 --- a/packages/manager/src/hooks/useInitialRequests.ts +++ b/packages/manager/src/hooks/useInitialRequests.ts @@ -88,7 +88,7 @@ export const useInitialRequests = () => { ]; // Start events polling - startEventsInterval(store, queryClient); + // startEventsInterval(store, queryClient); try { await Promise.all(dataFetchingPromises); diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 0f641489416..3d100642018 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -1,5 +1,9 @@ import { getEvents } from '@linode/api-v4'; -import { useInfiniteQuery } from 'react-query'; +import { useInfiniteQuery, useQuery, useQueryClient } from 'react-query'; + +import { DISABLE_EVENT_THROTTLE, INTERVAL } from 'src/constants'; +import { isInProgressEvent } from 'src/store/events/event.helpers'; +import { generatePollingFilter } from 'src/utilities/requestFilters'; import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; @@ -14,9 +18,61 @@ export const useEventsInfiniteQuery = (filter: Filter = {}) => { ); const events = query.data?.pages.reduce( - (events, page) => [...events, ...page.data], + (events, page) => [...page.data, ...events], [] ); return { ...query, events }; }; + +export const useInProgressEvents = () => { + return useQuery, APIError[]>({ + queryKey: ['events', 'poller'], + }); +}; + +export const useEventsPoller = () => { + const { incrementPollingInterval, pollingInterval } = usePollingInterval(); + + const { events } = useEventsInfiniteQuery(); + + const latestEventTime = events ? events[0].created : ''; + + const inProgressEvents = events?.filter(isInProgressEvent); + + const hasFetchedInitialEvents = events !== undefined; + + const filter = generatePollingFilter( + latestEventTime, + inProgressEvents?.map((event) => event.id) + ); + + useQuery({ + enabled: hasFetchedInitialEvents, + onSuccess() { + incrementPollingInterval(); + }, + queryFn: () => getEvents({}, filter), + queryKey: ['events', 'poller'], + refetchInterval: pollingInterval, + }); +}; + +const usePollingInterval = () => { + const queryKey = ['events', 'interval']; + const queryClient = useQueryClient(); + const { data: intervalMultiplier = 1 } = useQuery(queryKey, () => + queryClient.getQueryData(queryKey) + ); + return { + incrementPollingInterval: () => + queryClient.setQueryData( + queryKey, + Math.min(intervalMultiplier + 1, 16) + ), + pollingInterval: DISABLE_EVENT_THROTTLE + ? 500 + : intervalMultiplier * INTERVAL, + resetPollingInterval: () => queryClient.setQueryData(queryKey, 1), + }; +}; From 94061629d0dc4ed1889e6248ef09c58f3c26b97c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 17:28:22 -0500 Subject: [PATCH 05/44] add event handlers and cache updater --- packages/manager/src/events.ts | 2 +- .../manager/src/hooks/useEventHandlers.ts | 2 +- packages/manager/src/queries/events.ts | 72 ++++++++++++++++++- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/events.ts b/packages/manager/src/events.ts index b1195fb578c..846b8b55860 100644 --- a/packages/manager/src/events.ts +++ b/packages/manager/src/events.ts @@ -29,7 +29,7 @@ import { ApplicationStore } from './store'; export interface EventWithStore { event: Event; queryClient: QueryClient; - store: ApplicationStore; + store?: ApplicationStore; } export interface EntityEventWithStore extends EventWithStore { diff --git a/packages/manager/src/hooks/useEventHandlers.ts b/packages/manager/src/hooks/useEventHandlers.ts index 39d360620dc..6d17e1890cf 100644 --- a/packages/manager/src/hooks/useEventHandlers.ts +++ b/packages/manager/src/hooks/useEventHandlers.ts @@ -14,7 +14,7 @@ import { supportTicketEventHandler } from 'src/queries/support'; import { tokenEventHandler } from 'src/queries/tokens'; import { volumeEventsHandler } from 'src/queries/volumes'; -const eventHandlers: { +export const eventHandlers: { filter: (event: EventWithStore) => boolean; handler: (event: EventWithStore) => void; }[] = [ diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 3d100642018..f462ef6f8b5 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -1,7 +1,13 @@ import { getEvents } from '@linode/api-v4'; -import { useInfiniteQuery, useQuery, useQueryClient } from 'react-query'; +import { + InfiniteData, + useInfiniteQuery, + useQuery, + useQueryClient, +} from 'react-query'; import { DISABLE_EVENT_THROTTLE, INTERVAL } from 'src/constants'; +import { eventHandlers } from 'src/hooks/useEventHandlers'; import { isInProgressEvent } from 'src/store/events/event.helpers'; import { generatePollingFilter } from 'src/utilities/requestFilters'; @@ -34,23 +40,83 @@ export const useInProgressEvents = () => { export const useEventsPoller = () => { const { incrementPollingInterval, pollingInterval } = usePollingInterval(); + const queryClient = useQueryClient(); + const { events } = useEventsInfiniteQuery(); const latestEventTime = events ? events[0].created : ''; + // @todo run a reducde to optimize const inProgressEvents = events?.filter(isInProgressEvent); + const eventsThatAlreadyHappenedAtTheFilterTime = events?.filter( + (event) => event.created === latestEventTime + ); const hasFetchedInitialEvents = events !== undefined; const filter = generatePollingFilter( latestEventTime, - inProgressEvents?.map((event) => event.id) + inProgressEvents?.map((event) => event.id), + eventsThatAlreadyHappenedAtTheFilterTime?.map((e) => e.id) ); useQuery({ enabled: hasFetchedInitialEvents, - onSuccess() { + onSuccess(events) { incrementPollingInterval(); + + const { existingEvents, newEvents } = events.data.reduce<{ + existingEvents: Event[]; + newEvents: Event[]; + }>( + (acc, event) => { + if (inProgressEvents?.some((e) => e.id === event.id)) { + acc.existingEvents.push(event); + } else { + acc.newEvents.push(event); + } + return acc; + }, + { existingEvents: [], newEvents: [] } + ); + + queryClient.setQueryData>>( + ['events', 'infinite', {}], + (prev) => { + const newPages = prev?.pages.map((page, index) => { + if (index === 0 && newEvents.length > 0) { + page.data = [...newEvents, ...page.data]; + + return page; + } + + for (const event of existingEvents) { + const indexOfEvent = page.data.findIndex( + (e) => e.id === event.id + ); + + if (indexOfEvent !== -1) { + page.data[index] = event; + } + } + + return page; + }); + + return { + pageParams: prev?.pageParams ?? [], + pages: newPages ?? [], + }; + } + ); + + for (const event of events.data) { + for (const eventHandler of eventHandlers) { + if (eventHandler.filter({ event, queryClient })) { + eventHandler.handler({ event, queryClient }); + } + } + } }, queryFn: () => getEvents({}, filter), queryKey: ['events', 'poller'], From f84beffe019c54b437a1863167a7f2d7710c37dc Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 17:47:18 -0500 Subject: [PATCH 06/44] hook up toasts --- packages/manager/src/App.tsx | 4 +- .../manager/src/hooks/useEventHandlers.ts | 59 ++++------ .../manager/src/hooks/useInitialRequests.ts | 3 - .../src/hooks/useToastNotifications.tsx | 108 ++++++++---------- packages/manager/src/queries/events.ts | 5 +- 5 files changed, 74 insertions(+), 105 deletions(-) diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index 688ec0e1ac0..3419f9e4c34 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -14,13 +14,12 @@ import { GoTo } from './GoTo'; import { MainContent } from './MainContent'; import { SplashScreen } from './components/SplashScreen'; import { useAdobeAnalytics } from './hooks/useAdobeAnalytics'; -import { useEventHandlers } from './hooks/useEventHandlers'; import { useGlobalKeyboardListener } from './hooks/useGlobalKeyboardListener'; import { useInitialRequests } from './hooks/useInitialRequests'; import { useNewRelic } from './hooks/useNewRelic'; import { useToastNotifications } from './hooks/useToastNotifications'; -import { useSetupFeatureFlags } from './useSetupFeatureFlags'; import { useEventsPoller } from './queries/events'; +import { useSetupFeatureFlags } from './useSetupFeatureFlags'; // Ensure component's display name is 'App' export const App = () => ; @@ -34,7 +33,6 @@ const BaseApp = withDocumentTitleProvider( const { goToOpen, setGoToOpen } = useGlobalKeyboardListener(); - useEventHandlers(); useEventsPoller(); useToastNotifications(); diff --git a/packages/manager/src/hooks/useEventHandlers.ts b/packages/manager/src/hooks/useEventHandlers.ts index 6d17e1890cf..4c7660b9b07 100644 --- a/packages/manager/src/hooks/useEventHandlers.ts +++ b/packages/manager/src/hooks/useEventHandlers.ts @@ -1,6 +1,3 @@ -import React from 'react'; - -import { EventWithStore, events$ } from 'src/events'; import { oauthClientsEventHandler } from 'src/queries/accountOAuth'; import { databaseEventsHandler } from 'src/queries/databases'; import { domainEventsHandler } from 'src/queries/domains'; @@ -14,87 +11,75 @@ import { supportTicketEventHandler } from 'src/queries/support'; import { tokenEventHandler } from 'src/queries/tokens'; import { volumeEventsHandler } from 'src/queries/volumes'; +import type { Event } from '@linode/api-v4'; +import type { QueryClient } from 'react-query'; + +export interface EventHandlerData { + event: Event; + queryClient: QueryClient; +} + export const eventHandlers: { - filter: (event: EventWithStore) => boolean; - handler: (event: EventWithStore) => void; + filter: (event: Event) => boolean; + handler: (data: EventHandlerData) => void; }[] = [ { - filter: ({ event }) => - event.action.startsWith('database') && !event._initial, + filter: (event) => event.action.startsWith('database') && !event._initial, handler: databaseEventsHandler, }, { - filter: ({ event }) => + filter: (event) => event.action.startsWith('domain') && !event._initial && event.entity !== null, handler: domainEventsHandler, }, { - filter: ({ event }) => event.action.startsWith('volume') && !event._initial, + filter: (event) => event.action.startsWith('volume') && !event._initial, handler: volumeEventsHandler, }, { - filter: ({ event }) => + filter: (event) => (event.action.startsWith('image') || event.action === 'disk_imagize') && !event._initial, handler: imageEventsHandler, }, { - filter: ({ event }) => event.action.startsWith('token') && !event._initial, + filter: (event) => event.action.startsWith('token') && !event._initial, handler: tokenEventHandler, }, { - filter: ({ event }) => + filter: (event) => event.action.startsWith('user_ssh_key') && !event._initial, handler: sshKeyEventHandler, }, { - filter: ({ event }) => - event.action.startsWith('firewall') && !event._initial, + filter: (event) => event.action.startsWith('firewall') && !event._initial, handler: firewallEventsHandler, }, { - filter: ({ event }) => + filter: (event) => event.action.startsWith('nodebalancer') && !event._initial, handler: nodebalanacerEventHandler, }, { - filter: ({ event }) => + filter: (event) => event.action.startsWith('oauth_client') && !event._initial, handler: oauthClientsEventHandler, }, { - filter: ({ event }) => + filter: (event) => (event.action.startsWith('linode') || event.action.startsWith('backups')) && !event._initial, handler: linodeEventsHandler, }, { - filter: ({ event }) => event.action.startsWith('ticket') && !event._initial, + filter: (event) => event.action.startsWith('ticket') && !event._initial, handler: supportTicketEventHandler, }, { - filter: ({ event }) => event.action.startsWith('disk') && !event._initial, + filter: (event) => event.action.startsWith('disk') && !event._initial, handler: diskEventHandler, }, ]; - -/** - * Subscribes to events and passes an event to the corresponding - * event handler as it comes in. - */ -export const useEventHandlers = () => { - React.useEffect(() => { - const subscriptions = eventHandlers.map(({ filter, handler }) => - events$.filter(filter).subscribe(handler) - ); - - return () => { - subscriptions.forEach((sub) => sub.unsubscribe()); - }; - }, []); - - return null; -}; diff --git a/packages/manager/src/hooks/useInitialRequests.ts b/packages/manager/src/hooks/useInitialRequests.ts index 00b5205ecc6..56a33529db7 100644 --- a/packages/manager/src/hooks/useInitialRequests.ts +++ b/packages/manager/src/hooks/useInitialRequests.ts @@ -2,9 +2,7 @@ import { getAccountInfo, getAccountSettings } from '@linode/api-v4/lib/account'; import { getProfile, getUserPreferences } from '@linode/api-v4/lib/profile'; import * as React from 'react'; import { useQueryClient } from 'react-query'; -import { useStore } from 'react-redux'; -import { startEventsInterval } from 'src/events'; import { useAuthentication } from 'src/hooks/useAuthentication'; import { usePendingUpload } from 'src/hooks/usePendingUpload'; import { queryKey as accountQueryKey } from 'src/queries/account'; @@ -16,7 +14,6 @@ import { redirectToLogin } from 'src/session'; * as we make our inital requests. */ export const useInitialRequests = () => { - const store = useStore(); const queryClient = useQueryClient(); const { token } = useAuthentication(); diff --git a/packages/manager/src/hooks/useToastNotifications.tsx b/packages/manager/src/hooks/useToastNotifications.tsx index bc4887796c6..91de38ca5d0 100644 --- a/packages/manager/src/hooks/useToastNotifications.tsx +++ b/packages/manager/src/hooks/useToastNotifications.tsx @@ -5,7 +5,6 @@ import 'rxjs/add/operator/filter'; import { Link } from 'src/components/Link'; import { SupportLink } from 'src/components/SupportLink'; -import { events$ } from 'src/events'; import { sendLinodeDiskEvent } from 'src/utilities/analytics'; export const getLabel = (event: Event) => event.entity?.label ?? ''; @@ -142,71 +141,58 @@ const toasts: Toasts = { }, }; -/** - * Subscribes to incoming events and displays a toast notification if - * one is defined in `toasts`. - */ export const useToastNotifications = () => { const { enqueueSnackbar } = useSnackbar(); - React.useEffect(() => { - const subscription = events$ - .filter(({ event }) => !event._initial) - .subscribe(({ event }) => { - const toastInfo = toasts[event.action]; - - if (!toastInfo) { - return; - } - - if ( - ['finished', 'notification'].includes(event.status) && - toastInfo.success - ) { - const successMessage = - typeof toastInfo.success === 'function' - ? toastInfo.success(event) - : toastInfo.success; - - enqueueSnackbar(successMessage, { - variant: 'success', - }); - } - - if (event.status === 'failed' && toastInfo.failure) { - const failureMessage = - typeof toastInfo.failure === 'function' - ? toastInfo.failure(event) - : toastInfo.failure; - - const hasSupportLink = - failureMessage?.includes('contact Support') ?? false; - - const formattedFailureMessage = ( - <> - {failureMessage?.replace(/ contact Support/i, '') ?? - failureMessage} - {hasSupportLink ? ( - <> -   - . - - ) : null} - {toastInfo.link ? <> {toastInfo.link} : null} - - ); + const handleGlobalToast = (event: Event) => { + const toastInfo = toasts[event.action]; + + if (!toastInfo) { + return; + } + + if ( + ['finished', 'notification'].includes(event.status) && + toastInfo.success + ) { + const successMessage = + typeof toastInfo.success === 'function' + ? toastInfo.success(event) + : toastInfo.success; - enqueueSnackbar(formattedFailureMessage, { - persist: toastInfo.persistFailureMessage, - variant: 'error', - }); - } + enqueueSnackbar(successMessage, { + variant: 'success', }); + } - return () => { - subscription.unsubscribe(); - }; - }, []); + if (event.status === 'failed' && toastInfo.failure) { + const failureMessage = + typeof toastInfo.failure === 'function' + ? toastInfo.failure(event) + : toastInfo.failure; + + const hasSupportLink = + failureMessage?.includes('contact Support') ?? false; + + const formattedFailureMessage = ( + <> + {failureMessage?.replace(/ contact Support/i, '') ?? failureMessage} + {hasSupportLink ? ( + <> +   + . + + ) : null} + {toastInfo.link ? <> {toastInfo.link} : null} + + ); + + enqueueSnackbar(formattedFailureMessage, { + persist: toastInfo.persistFailureMessage, + variant: 'error', + }); + } + }; - return null; + return { handleGlobalToast }; }; diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index f462ef6f8b5..f5647fbd547 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -8,6 +8,7 @@ import { import { DISABLE_EVENT_THROTTLE, INTERVAL } from 'src/constants'; import { eventHandlers } from 'src/hooks/useEventHandlers'; +import { useToastNotifications } from 'src/hooks/useToastNotifications'; import { isInProgressEvent } from 'src/store/events/event.helpers'; import { generatePollingFilter } from 'src/utilities/requestFilters'; @@ -40,6 +41,7 @@ export const useInProgressEvents = () => { export const useEventsPoller = () => { const { incrementPollingInterval, pollingInterval } = usePollingInterval(); + const { handleGlobalToast } = useToastNotifications(); const queryClient = useQueryClient(); const { events } = useEventsInfiniteQuery(); @@ -112,10 +114,11 @@ export const useEventsPoller = () => { for (const event of events.data) { for (const eventHandler of eventHandlers) { - if (eventHandler.filter({ event, queryClient })) { + if (eventHandler.filter(event)) { eventHandler.handler({ event, queryClient }); } } + handleGlobalToast(event); } }, queryFn: () => getEvents({}, filter), From 3b0b1b87e7c078537bf19d33ce634bca095c6e06 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 19:57:25 -0500 Subject: [PATCH 07/44] fix bug by removing early return and start refactoring --- .../ApiAwarenessModal/ApiAwarenessModal.tsx | 14 +++++++------- .../Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx | 8 ++++++-- packages/manager/src/queries/events.ts | 2 -- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx b/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx index c495a971c25..39ca70d72cc 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx @@ -7,12 +7,12 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Dialog } from 'src/components/Dialog/Dialog'; import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; +import { TabPanels } from 'src/components/ReachTabPanels'; +import { Tabs } from 'src/components/ReachTabs'; import { SafeTabPanel } from 'src/components/SafeTabPanel/SafeTabPanel'; import { TabLinkList } from 'src/components/TabLinkList/TabLinkList'; import { Typography } from 'src/components/Typography'; -import { TabPanels } from 'src/components/ReachTabPanels'; -import { Tabs } from 'src/components/ReachTabs'; -import useEvents from 'src/hooks/useEvents'; +import { useInProgressEvents } from 'src/queries/events'; import { sendApiAwarenessClickEvent } from 'src/utilities/analytics'; import { generateCurlCommand } from 'src/utilities/generate-cURL'; import { generateCLICommand } from 'src/utilities/generate-cli'; @@ -31,16 +31,16 @@ export const ApiAwarenessModal = (props: ApiAwarenessModalProps) => { const theme = useTheme(); const history = useHistory(); - const { events } = useEvents(); + const { data: events } = useInProgressEvents(); - const createdLinode = events.filter( + const linodeCreationEvent = events?.data.find( (event) => (event.action === 'linode_create' || event.action === 'linode_clone') && event.entity?.label === payLoad.label && (event.status === 'scheduled' || event.status === 'started') ); - const isLinodeCreated = createdLinode.length === 1; + const isLinodeCreated = linodeCreationEvent !== undefined; const curlCommand = useMemo( () => generateCurlCommand(payLoad, '/linode/instances'), @@ -69,7 +69,7 @@ export const ApiAwarenessModal = (props: ApiAwarenessModalProps) => { useEffect(() => { if (isLinodeCreated && isOpen) { onClose(); - history.replace(`/linodes/${createdLinode[0].entity?.id}`); + history.replace(`/linodes/${linodeCreationEvent.entity?.id}`); } }, [isLinodeCreated]); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx index 9917d23f94d..0578dca90b5 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx @@ -18,8 +18,8 @@ import { } from 'src/features/Linodes/transitions'; import { notificationContext as _notificationContext } from 'src/features/NotificationCenter/NotificationContext'; import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; +import { useInProgressEvents } from 'src/queries/events'; import { useTypeQuery } from 'src/queries/types'; -import { useRecentEventForLinode } from 'src/store/selectors/recentEventForLinode'; import { capitalizeAllWords } from 'src/utilities/capitalize'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { LinodeWithMaintenance } from 'src/utilities/linodes'; @@ -54,7 +54,11 @@ export const LinodeRow = (props: Props) => { const { data: linodeType } = useTypeQuery(type ?? '', type !== null); - const recentEvent = useRecentEventForLinode(id); + const { data: events } = useInProgressEvents(); + + const recentEvent = events?.data.find( + (e) => e.entity?.type === 'linode' && e.entity.id === id + ); const { isVPCOnlyLinode } = useVPCConfigInterface(id); diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index f5647fbd547..543f657fd8d 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -88,8 +88,6 @@ export const useEventsPoller = () => { const newPages = prev?.pages.map((page, index) => { if (index === 0 && newEvents.length > 0) { page.data = [...newEvents, ...page.data]; - - return page; } for (const event of existingEvents) { From 37b94b548812c34d51370e92a8c05eb3ab5962da Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 21:51:04 -0500 Subject: [PATCH 08/44] refactor event consumers --- .../NodePoolsDisplay/NodeTable.tsx | 9 ++++- .../features/Linodes/LinodeEntityDetail.tsx | 8 +++- .../ApiAwarenessModal/ApiAwarenessModal.tsx | 2 +- .../LinodeStorage/LinodeDiskRow.tsx | 6 +-- .../LinodesLanding/LinodeRow/LinodeRow.tsx | 2 +- .../Linodes/LinodesLanding/withRecentEvent.ts | 18 --------- .../Linodes/MigrateLinode/MigrateLinode.tsx | 6 +-- .../useEventNotifications.tsx | 6 ++- .../src/features/Volumes/VolumeTableRow.tsx | 9 ----- packages/manager/src/hooks/useEvents.ts | 22 ---------- packages/manager/src/queries/events.ts | 8 ++-- .../selectors/recentEventForLinode.test.ts | 40 ------------------- .../store/selectors/recentEventForLinode.ts | 39 ------------------ 13 files changed, 29 insertions(+), 146 deletions(-) delete mode 100644 packages/manager/src/features/Linodes/LinodesLanding/withRecentEvent.ts delete mode 100644 packages/manager/src/hooks/useEvents.ts delete mode 100644 packages/manager/src/store/selectors/recentEventForLinode.test.ts delete mode 100644 packages/manager/src/store/selectors/recentEventForLinode.ts diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx index 0fb5940d499..4661801e747 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx @@ -21,8 +21,8 @@ import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; import { Typography } from 'src/components/Typography'; import { transitionText } from 'src/features/Linodes/transitions'; +import { useInProgressEvents } from 'src/queries/events'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; -import { useRecentEventForLinode } from 'src/store/selectors/recentEventForLinode'; import { LinodeWithMaintenance } from 'src/utilities/linodes'; import NodeActionMenu from './NodeActionMenu'; @@ -217,7 +217,12 @@ export const NodeRow: React.FC = React.memo((props) => { const classes = useStyles(); - const recentEvent = useRecentEventForLinode(instanceId ?? -1); + const { data: events } = useInProgressEvents(); + + const recentEvent = events?.find( + (event) => + event.entity?.id === instanceId && event.entity?.type === 'linode' + ); const linodeLink = instanceId ? `/linodes/${instanceId}` : undefined; diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx index 5b2a489d78f..0edd5ee6a33 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx @@ -31,7 +31,6 @@ import { useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { useTypeQuery } from 'src/queries/types'; import { useLinodeVolumesQuery } from 'src/queries/volumes'; -import { useRecentEventForLinode } from 'src/store/selectors/recentEventForLinode'; import { sendLinodeActionMenuItemEvent } from 'src/utilities/analytics'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { formatDate } from 'src/utilities/formatDate'; @@ -68,6 +67,7 @@ import type { LinodeType, } from '@linode/api-v4/lib/linodes/types'; import type { Subnet } from '@linode/api-v4/lib/vpcs'; +import { useInProgressEvents } from 'src/queries/events'; interface LinodeEntityDetailProps { id: number; @@ -86,7 +86,11 @@ export const LinodeEntityDetail = (props: Props) => { const notificationContext = React.useContext(_notificationContext); - const recentEvent = useRecentEventForLinode(linode.id); + const { data: events } = useInProgressEvents(); + + const recentEvent = events?.find( + (event) => event.entity?.id === linode.id && event.entity.type === 'linode' + ); const { data: images } = useAllImagesQuery({}, {}); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx b/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx index 39ca70d72cc..582545b328a 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx @@ -33,7 +33,7 @@ export const ApiAwarenessModal = (props: ApiAwarenessModalProps) => { const history = useHistory(); const { data: events } = useInProgressEvents(); - const linodeCreationEvent = events?.data.find( + const linodeCreationEvent = events?.find( (event) => (event.action === 'linode_create' || event.action === 'linode_clone') && event.entity?.label === payLoad.label && diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx index c87b7f0447f..69bc18d40e7 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx @@ -7,7 +7,7 @@ import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { Hidden } from 'src/components/Hidden'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import { useEvents } from 'src/hooks/useEvents'; +import { useInProgressEvents } from 'src/queries/events'; import { LinodeDiskActionMenu } from './LinodeDiskActionMenu'; @@ -23,7 +23,7 @@ interface Props { } export const LinodeDiskRow = React.memo((props: Props) => { - const { inProgressEvents } = useEvents(); + const { data: events } = useInProgressEvents(); const theme = useTheme(); const { disk, @@ -44,7 +44,7 @@ export const LinodeDiskRow = React.memo((props: Props) => { const diskEventsToShowProgressFor = Object.keys(diskEventLabelMap); - const event = inProgressEvents.find( + const event = events?.find( (event) => event.secondary_entity?.id === disk.id && diskEventsToShowProgressFor.includes(event.action) diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx index 0578dca90b5..a9c1b317e81 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx @@ -56,7 +56,7 @@ export const LinodeRow = (props: Props) => { const { data: events } = useInProgressEvents(); - const recentEvent = events?.data.find( + const recentEvent = events?.find( (e) => e.entity?.type === 'linode' && e.entity.id === id ); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/withRecentEvent.ts b/packages/manager/src/features/Linodes/LinodesLanding/withRecentEvent.ts deleted file mode 100644 index ba706119d35..00000000000 --- a/packages/manager/src/features/Linodes/LinodesLanding/withRecentEvent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Event } from '@linode/api-v4/lib/account'; -import { connect } from 'react-redux'; - -import recentEventForLinode from 'src/store/selectors/recentEventForLinode'; -import { MapState } from 'src/store/types'; - -export interface WithRecentEvent { - recentEvent?: Event; -} - -const mapStateToProps: MapState = ( - state, - props -) => ({ - recentEvent: recentEventForLinode(props.id)(state), -}); - -export default connect(mapStateToProps); diff --git a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx index 8cc1f34c649..6503309e32a 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx @@ -13,13 +13,13 @@ import { MBpsInterDC } from 'src/constants'; import { resetEventsPolling } from 'src/eventsPolling'; import { EUAgreementCheckbox } from 'src/features/Account/Agreements/EUAgreementCheckbox'; import { regionSupportsMetadata } from 'src/features/Linodes/LinodesCreate/utilities'; -import useEvents from 'src/hooks/useEvents'; import { useFlags } from 'src/hooks/useFlags'; import { reportAgreementSigningError, useAccountAgreements, useMutateAccountAgreements, } from 'src/queries/accountAgreements'; +import { useInProgressEvents } from 'src/queries/events'; import { useImageQuery } from 'src/queries/images'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { @@ -72,10 +72,10 @@ export const MigrateLinode = React.memo((props: Props) => { linodeId !== undefined && open ); - const { events } = useEvents(); + const { data: events } = useInProgressEvents(); const eventsForLinode = linodeId - ? events.filter((event) => isEventRelevantToLinode(event, linodeId)) + ? events?.filter((event) => isEventRelevantToLinode(event, linodeId)) ?? [] : []; const { diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx b/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx index 3c20b6a712c..2ca94bd5524 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx @@ -2,7 +2,7 @@ import { Event, EventAction } from '@linode/api-v4/lib/account/types'; import { partition } from 'ramda'; import * as React from 'react'; -import useEvents from 'src/hooks/useEvents'; +import { useEventsInfiniteQuery } from 'src/queries/events'; import { isInProgressEvent } from 'src/store/events/event.helpers'; import { ExtendedEvent } from 'src/store/events/event.types'; import { removeBlocklistedEvents } from 'src/utilities/eventUtils'; @@ -22,7 +22,9 @@ const unwantedEvents: EventAction[] = [ ]; export const useEventNotifications = (givenEvents?: ExtendedEvent[]) => { - const events = removeBlocklistedEvents(givenEvents ?? useEvents().events); + const events = removeBlocklistedEvents( + givenEvents ?? useEventsInfiniteQuery().events + ); const notificationContext = React.useContext(_notificationContext); const _events = events.filter( diff --git a/packages/manager/src/features/Volumes/VolumeTableRow.tsx b/packages/manager/src/features/Volumes/VolumeTableRow.tsx index a6efc1184b5..1fb8977bb63 100644 --- a/packages/manager/src/features/Volumes/VolumeTableRow.tsx +++ b/packages/manager/src/features/Volumes/VolumeTableRow.tsx @@ -14,7 +14,6 @@ import { Typography } from 'src/components/Typography'; import { useRegionsQuery } from 'src/queries/regions'; import { ActionHandlers, VolumesActionMenu } from './VolumesActionMenu'; -// import useEvents from 'src/hooks/useEvents'; export const useStyles = makeStyles({ chipWrapper: { @@ -74,14 +73,6 @@ export const VolumeTableRow = React.memo((props: Props) => { const regionLabel = regions?.find((r) => r.id === volume.region)?.label ?? volume.region; - // const { events } = useEvents(); - - // const recentEvent = events.find((event) => event.entity?.id === id); - - // Use this to show a progress bar - // const isUpdating = isVolumeUpdating(recentEvent); - // const progress = progressFromEvent(recentEvent); - return ( diff --git a/packages/manager/src/hooks/useEvents.ts b/packages/manager/src/hooks/useEvents.ts deleted file mode 100644 index f9f7d99aa47..00000000000 --- a/packages/manager/src/hooks/useEvents.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useSelector } from 'react-redux'; - -import { ApplicationState } from 'src/store'; -import { ExtendedEvent } from 'src/store/events/event.types'; - -export interface UseEvents { - events: ExtendedEvent[]; - inProgressEvents: ExtendedEvent[]; -} - -export const useEvents = () => { - const eventState = useSelector((state: ApplicationState) => state.events); - const events = eventState.events; - const inProgressIDs = Object.keys(eventState.inProgressEvents); - const inProgressEvents = events.filter((thisEvent) => - inProgressIDs.includes(String(thisEvent.id)) - ); - - return { events, inProgressEvents }; -}; - -export default useEvents; diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 543f657fd8d..7f3cb1e9941 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -33,7 +33,7 @@ export const useEventsInfiniteQuery = (filter: Filter = {}) => { }; export const useInProgressEvents = () => { - return useQuery, APIError[]>({ + return useQuery({ queryKey: ['events', 'poller'], }); }; @@ -67,7 +67,7 @@ export const useEventsPoller = () => { onSuccess(events) { incrementPollingInterval(); - const { existingEvents, newEvents } = events.data.reduce<{ + const { existingEvents, newEvents } = events.reduce<{ existingEvents: Event[]; newEvents: Event[]; }>( @@ -110,7 +110,7 @@ export const useEventsPoller = () => { } ); - for (const event of events.data) { + for (const event of events) { for (const eventHandler of eventHandlers) { if (eventHandler.filter(event)) { eventHandler.handler({ event, queryClient }); @@ -119,7 +119,7 @@ export const useEventsPoller = () => { handleGlobalToast(event); } }, - queryFn: () => getEvents({}, filter), + queryFn: () => getEvents({}, filter).then((data) => data.data), queryKey: ['events', 'poller'], refetchInterval: pollingInterval, }); diff --git a/packages/manager/src/store/selectors/recentEventForLinode.test.ts b/packages/manager/src/store/selectors/recentEventForLinode.test.ts deleted file mode 100644 index 9133fffef6e..00000000000 --- a/packages/manager/src/store/selectors/recentEventForLinode.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { entityFactory, eventFactory } from 'src/factories/events'; - -import recentEventForLinode from './recentEventForLinode'; - -describe('recentEventForLinode selector', () => { - const selector = recentEventForLinode(1); - const entity = entityFactory.build({ - id: 1, - }); - - it('only returns in-progress events', () => { - const nonInProgressEvent = eventFactory.build({ - entity, - percent_complete: null, - }); - const inProgressEvent = eventFactory.build({ - entity, - percent_complete: 50, - }); - const mockState1: any = { events: { events: [nonInProgressEvent] } }; - const mockState2: any = { events: { events: [inProgressEvent] } }; - expect(selector(mockState1)).toBeUndefined(); - expect(selector(mockState2)).toBeDefined(); - }); - - it('only returns events relevant to the Linode', () => { - const irrelevantEvent = eventFactory.build({ - entity: entityFactory.build({ - id: 2, - }), - }); - const relevantEvent = eventFactory.build({ - entity, - }); - const mockState1: any = { events: { events: [irrelevantEvent] } }; - const mockState2: any = { events: { events: [relevantEvent] } }; - expect(selector(mockState1)).toBeUndefined(); - expect(selector(mockState2)).toBeDefined(); - }); -}); diff --git a/packages/manager/src/store/selectors/recentEventForLinode.ts b/packages/manager/src/store/selectors/recentEventForLinode.ts deleted file mode 100644 index 4ea56ea2b6f..00000000000 --- a/packages/manager/src/store/selectors/recentEventForLinode.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; - -import { ApplicationState } from 'src/store'; - -import { isInProgressEvent } from '../events/event.helpers'; -import { isEventRelevantToLinode } from '../events/event.selectors'; - -export const useRecentEventForLinode = (linodeId: number) => { - const events = useSelector((state: ApplicationState) => state.events.events); - let idx = 0; - const len = events.length; - for (; idx < len; idx += 1) { - const event = events[idx]; - if (isInProgressEvent(event) && isEventRelevantToLinode(event, linodeId)) { - return event; - } - } - return undefined; -}; - -export default (linodeId: number) => - createSelector( - (state: ApplicationState) => state.events.events, - (events) => { - let idx = 0; - const len = events.length; - for (; idx < len; idx += 1) { - const event = events[idx]; - if ( - isInProgressEvent(event) && - isEventRelevantToLinode(event, linodeId) - ) { - return event; - } - } - return undefined; - } - ); From f32ecadc8c49aa4e28f333ddb65a83a6a6d40312 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 30 Nov 2023 23:56:52 -0500 Subject: [PATCH 09/44] finish general refactor --- packages/manager/package.json | 1 - packages/manager/src/App.test.tsx | 3 +- packages/manager/src/__data__/events.ts | 4 +- .../src/containers/events.container.ts | 18 +++ packages/manager/src/events.ts | 111 ----------------- packages/manager/src/eventsPolling.ts | 16 --- .../src/features/Domains/DomainRecords.tsx | 3 - .../Images/ImagesCreate/CreateImageTab.tsx | 4 +- .../src/features/Images/ImagesDrawer.tsx | 4 +- .../src/features/Images/ImagesLanding.tsx | 21 ++-- .../Linodes/CloneLanding/CloneLanding.tsx | 4 +- .../LinodesCreate/LinodeCreateContainer.tsx | 12 +- .../TabbedContent/FromImageContent.test.tsx | 3 +- .../TabbedContent/FromLinodeContent.test.tsx | 3 +- .../FromStackScriptContent.test.tsx | 3 +- .../LinodeBackup/CancelBackupsDialog.tsx | 4 +- .../LinodeBackup/CaptureSnapshot.tsx | 8 +- .../LinodeBackup/EnableBackupsDialog.tsx | 4 +- .../LinodeBackup/RestoreToLinodeDrawer.tsx | 4 +- .../LinodeConfigs/BootConfigDialog.tsx | 4 +- .../LinodeRebuild/RebuildFromImage.tsx | 4 +- .../LinodeRebuild/RebuildFromStackScript.tsx | 4 +- .../LinodeRescue/BareMetalRescue.tsx | 4 +- .../LinodeRescue/StandardRescueDialog.tsx | 4 +- .../LinodeResize/LinodeResize.tsx | 4 +- .../LinodeSettingsDeletePanel.tsx | 4 +- .../LinodeStorage/CreateDiskDrawer.tsx | 4 +- .../LinodeStorage/ResizeDiskDrawer.tsx | 8 +- .../MutationNotification.tsx | 4 +- .../LinodesLanding/DeleteLinodeDialog.tsx | 4 +- .../Linodes/LinodesLanding/LinodesLanding.tsx | 22 +--- .../Linodes/MigrateLinode/MigrateLinode.tsx | 7 +- .../Linodes/PowerActionsDialogOrDrawer.tsx | 4 +- .../manager/src/features/Linodes/index.tsx | 5 + .../src/features/Linodes/transitions.ts | 11 +- .../useEventNotifications.tsx | 9 +- .../useFormattedNotifications.tsx | 1 + .../NotificationSection.tsx | 1 + .../NotificationMenu/NotificationMenu.tsx | 8 +- .../features/Volumes/AttachVolumeDrawer.tsx | 4 +- .../features/Volumes/CloneVolumeDrawer.tsx | 4 +- .../features/Volumes/DeleteVolumeDialog.tsx | 4 +- .../features/Volumes/DetachVolumeDialog.tsx | 4 +- .../features/Volumes/ResizeVolumeDrawer.tsx | 4 +- .../VolumeDrawer/LinodeVolumeAttachForm.tsx | 4 +- .../VolumeDrawer/LinodeVolumeCreateForm.tsx | 4 +- .../src/hooks/useToastNotifications.tsx | 1 - packages/manager/src/index.tsx | 2 +- packages/manager/src/queries/accountOAuth.ts | 4 +- packages/manager/src/queries/databases.ts | 4 +- packages/manager/src/queries/domains.ts | 4 +- packages/manager/src/queries/events.ts | 48 +++++++- packages/manager/src/queries/firewalls.ts | 4 +- packages/manager/src/queries/images.ts | 4 +- .../manager/src/queries/linodes/events.ts | 9 +- packages/manager/src/queries/nodebalancers.ts | 4 +- packages/manager/src/queries/profile.ts | 5 +- packages/manager/src/queries/support.ts | 6 +- packages/manager/src/queries/tokens.ts | 5 +- packages/manager/src/queries/volumes.ts | 7 +- packages/manager/src/request.test.tsx | 3 +- .../authentication/authentication.test.ts | 3 +- .../manager/src/store/events/event.actions.ts | 19 --- .../src/store/events/event.helpers.test.ts | 76 +++++++++++- .../manager/src/store/events/event.helpers.ts | 36 +++++- .../src/store/events/event.reducer.test.ts | 87 -------------- .../manager/src/store/events/event.reducer.ts | 84 ------------- .../manager/src/store/events/event.request.ts | 113 ------------------ .../src/store/events/event.selectors.test.ts | 73 ----------- .../src/store/events/event.selectors.ts | 35 ------ .../manager/src/store/events/event.types.ts | 10 -- packages/manager/src/store/index.ts | 29 +---- .../middleware/combineEventsMiddleware.ts | 95 --------------- .../src/store/selectors/imageEvents.ts | 23 ---- .../src/store/selectors/inProgressEvents.ts | 13 -- .../src/utilities/promiseToObservable.ts | 33 ----- .../manager/src/utilities/testHelpers.tsx | 7 +- yarn.lock | 12 -- 78 files changed, 339 insertions(+), 893 deletions(-) create mode 100644 packages/manager/src/containers/events.container.ts delete mode 100644 packages/manager/src/events.ts delete mode 100644 packages/manager/src/eventsPolling.ts delete mode 100644 packages/manager/src/store/events/event.actions.ts delete mode 100644 packages/manager/src/store/events/event.reducer.test.ts delete mode 100644 packages/manager/src/store/events/event.reducer.ts delete mode 100644 packages/manager/src/store/events/event.request.ts delete mode 100644 packages/manager/src/store/events/event.selectors.test.ts delete mode 100644 packages/manager/src/store/events/event.selectors.ts delete mode 100644 packages/manager/src/store/events/event.types.ts delete mode 100644 packages/manager/src/store/middleware/combineEventsMiddleware.ts delete mode 100644 packages/manager/src/store/selectors/imageEvents.ts delete mode 100644 packages/manager/src/store/selectors/inProgressEvents.ts delete mode 100644 packages/manager/src/utilities/promiseToObservable.ts diff --git a/packages/manager/package.json b/packages/manager/package.json index d687a3d70d8..0b16a1bad80 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -69,7 +69,6 @@ "redux": "^4.0.4", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", - "rxjs": "^5.5.6", "sanitize-html": "^2.11.0", "search-string": "^3.1.0", "simple-git": "^3.19.0", diff --git a/packages/manager/src/App.test.tsx b/packages/manager/src/App.test.tsx index d9410cf2872..c8ffbf8697e 100644 --- a/packages/manager/src/App.test.tsx +++ b/packages/manager/src/App.test.tsx @@ -5,10 +5,9 @@ import { StaticRouter } from 'react-router-dom'; import { App } from './App'; import { LinodeThemeWrapper } from './LinodeThemeWrapper'; -import { queryClientFactory } from './queries/base'; import { storeFactory } from './store'; -const store = storeFactory(queryClientFactory()); +const store = storeFactory(); it('renders without crashing.', () => { const component = shallow( diff --git a/packages/manager/src/__data__/events.ts b/packages/manager/src/__data__/events.ts index e24ac3bf75f..0f00ce5052b 100644 --- a/packages/manager/src/__data__/events.ts +++ b/packages/manager/src/__data__/events.ts @@ -1,7 +1,5 @@ import { Event } from '@linode/api-v4/lib/account'; -import { ExtendedEvent } from 'src/store/events/event.types'; - export const events: Event[] = [ { action: 'linode_reboot', @@ -575,7 +573,7 @@ export const uniqueEvents: Event[] = [ }, ]; -export const reduxEvent: ExtendedEvent = { +export const reduxEvent: Event = { _initial: false, action: 'linode_boot', created: '2018-12-02T20:23:43', diff --git a/packages/manager/src/containers/events.container.ts b/packages/manager/src/containers/events.container.ts new file mode 100644 index 00000000000..a769af42f60 --- /dev/null +++ b/packages/manager/src/containers/events.container.ts @@ -0,0 +1,18 @@ +import React from 'react'; + +import { usePollingInterval } from 'src/queries/events'; + +export type WithPollingIntervalProps = ReturnType; + +export const withPollingInterval = ( + Component: React.ComponentType +) => { + return (props: Props) => { + const polling = usePollingInterval(); + + return React.createElement(Component, { + ...props, + ...polling, + }); + }; +}; diff --git a/packages/manager/src/events.ts b/packages/manager/src/events.ts deleted file mode 100644 index 846b8b55860..00000000000 --- a/packages/manager/src/events.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * This file fills two needs; - * Maintain the polling interval for events, - * Push events from Redux onto the events$ stream. - * - * The pushing of events onto events$ is maintaining the existing API between Redux and components - * which consume the events$ stream. Once all of the consumers of events$ have been updated - * to consume directly from the Redux store, we can delete the events$ stream. - * - * The next step is to move this logic into a Redux connected component so we can more easily - * access Redux and control the start of the event loop. - */ -import { EntityEvent, Event } from '@linode/api-v4/lib/account'; -import { QueryClient } from 'react-query'; -import { Subject } from 'rxjs/Subject'; - -import { DISABLE_EVENT_THROTTLE, INTERVAL } from 'src/constants'; -import { - getPollingInterval, - getRequestDeadline, - setPollingInterval, - setRequestDeadline, -} from 'src/eventsPolling'; -import { getEvents } from 'src/store/events/event.request'; -import { ThunkDispatch } from 'src/store/types'; - -import { ApplicationStore } from './store'; - -export interface EventWithStore { - event: Event; - queryClient: QueryClient; - store?: ApplicationStore; -} - -export interface EntityEventWithStore extends EventWithStore { - event: EntityEvent; -} - -export const events$ = new Subject(); - -let inProgress = false; - -export const requestEvents = ( - store: ApplicationStore, - queryClient: QueryClient -) => { - inProgress = true; - return (store.dispatch as ThunkDispatch)(getEvents()) - .then((events: Event[]) => { - const reversed = events.reverse(); - - /** - * This feeds the stream for consumers of events$. We're simply pushing the events from the - * request response onto the stream one at a time. - */ - reversed.forEach((event: Event) => { - events$.next({ event, queryClient, store }); - }); - inProgress = false; - }) - .catch((e) => e); -}; - -export const startEventsInterval = ( - store: ApplicationStore, - queryClient: QueryClient -) => - setInterval( - () => { - const now = Date.now(); - const pollIteration = getPollingInterval(); - const eventRequestDeadline = getRequestDeadline(); - - if (now > eventRequestDeadline) { - /** - * If we're waiting on a request, set reset the pollIteration and return to prevent - * overlapping requests. - */ - if (inProgress) { - /** leaving this commented out for now because I'm not sure if it'll break anything */ - // pollIteration = 1; - return; - } - - if (document.visibilityState !== 'visible') { - // don't request events in an inactive/minimized tab. - return; - } - - requestEvents(store, queryClient); - - if (DISABLE_EVENT_THROTTLE) { - /* - * If throttling is disabled manually set the timeout so tests wait to query the mock data store. - */ - setRequestDeadline(now + 500); - } else { - const timeout = INTERVAL * pollIteration; - /** Update the deadline */ - setRequestDeadline(now + timeout); - /* Update the iteration to a maximum of 16. */ - const newIteration = Math.min(pollIteration * 2, 16); - if (pollIteration < 16) { - setPollingInterval(newIteration); - } - } - } - }, - /* the following is the Nyquist rate for the minimum polling interval */ - INTERVAL / 2 - 1 - ); diff --git a/packages/manager/src/eventsPolling.ts b/packages/manager/src/eventsPolling.ts deleted file mode 100644 index 8a4986f26e4..00000000000 --- a/packages/manager/src/eventsPolling.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { INTERVAL } from 'src/constants'; - -let interval = 1; -let deadline = Date.now(); -export const resetEventsPolling = (newInterval: number = 1) => { - deadline = Date.now() + INTERVAL * newInterval; - interval = newInterval; -}; - -export const setRequestDeadline = (newDeadline: number) => - (deadline = newDeadline); -export const setPollingInterval = (newInterval: number) => - (interval = newInterval); - -export const getRequestDeadline = () => deadline; -export const getPollingInterval = () => interval; diff --git a/packages/manager/src/features/Domains/DomainRecords.tsx b/packages/manager/src/features/Domains/DomainRecords.tsx index aaad7f77df1..ff5ed92a118 100644 --- a/packages/manager/src/features/Domains/DomainRecords.tsx +++ b/packages/manager/src/features/Domains/DomainRecords.tsx @@ -22,7 +22,6 @@ import { } from 'ramda'; import * as React from 'react'; import { compose as recompose } from 'recompose'; -import { Subscription } from 'rxjs/Subscription'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Button } from 'src/components/Button/Button'; @@ -330,8 +329,6 @@ class DomainRecords extends React.Component { this.updateConfirmDialog((c) => ({ ...c, submitting: true })); }; - eventsSubscription$: Subscription; - generateTypes = (): IType[] => [ /** SOA Record */ { diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index 608d5580ba8..a11e4a8ccc7 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -15,10 +15,10 @@ import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; import { DiskSelect } from 'src/features/Linodes/DiskSelect/DiskSelect'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useFlags } from 'src/hooks/useFlags'; +import { usePollingInterval } from 'src/queries/events'; import { useCreateImageMutation } from 'src/queries/images'; import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -102,6 +102,8 @@ export const CreateImageTab: React.FC = (props) => { const { mutateAsync: createImage } = useCreateImageMutation(); + const { resetEventsPolling } = usePollingInterval(); + const [selectedLinode, setSelectedLinode] = React.useState(); const [selectedDisk, setSelectedDisk] = React.useState(''); const [disks, setDisks] = React.useState([]); diff --git a/packages/manager/src/features/Images/ImagesDrawer.tsx b/packages/manager/src/features/Images/ImagesDrawer.tsx index 6e6132d77f1..d167a0ad861 100644 --- a/packages/manager/src/features/Images/ImagesDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesDrawer.tsx @@ -13,7 +13,6 @@ import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; import { IMAGE_DEFAULT_LIMIT } from 'src/constants'; -import { resetEventsPolling } from 'src/eventsPolling'; import { DiskSelect } from 'src/features/Linodes/DiskSelect/DiskSelect'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { @@ -24,6 +23,7 @@ import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { useImageAndLinodeGrantCheck } from './utils'; +import { usePollingInterval } from 'src/queries/events'; const useStyles = makeStyles((theme: Theme) => ({ actionPanel: { @@ -102,6 +102,8 @@ export const ImagesDrawer = (props: CombinedProps) => { permissionedLinodes: availableLinodes, } = useImageAndLinodeGrantCheck(); + const { resetEventsPolling } = usePollingInterval(); + const [mounted, setMounted] = React.useState(false); const [notice, setNotice] = React.useState(undefined); const [submitting, setSubmitting] = React.useState(false); diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index c7960d2d840..8c0d1815913 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -6,7 +6,6 @@ import produce from 'immer'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { useQueryClient } from 'react-query'; -import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; @@ -30,14 +29,17 @@ import { Typography } from 'src/components/Typography'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { listToItemsByID } from 'src/queries/base'; +import { useEventsInfiniteQuery } from 'src/queries/events'; import { queryKey, removeImageFromCache, useDeleteImageMutation, useImagesQuery, } from 'src/queries/images'; -import { ApplicationState } from 'src/store'; -import imageEvents from 'src/store/selectors/imageEvents'; +import { + isEventImageUpload, + isEventInProgressDiskImagize, +} from 'src/store/events/event.helpers'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import ImageRow, { ImageWithEvent } from './ImageRow'; @@ -185,19 +187,24 @@ export const ImagesLanding: React.FC = () => { const { mutateAsync: deleteImage } = useDeleteImageMutation(); - const eventState = useSelector((state: ApplicationState) => state.events); - const events = imageEvents(eventState); + const { events } = useEventsInfiniteQuery(); + + const imageEvents = + events?.filter( + (event) => + isEventInProgressDiskImagize(event) || isEventImageUpload(event) + ) ?? []; // Private images with the associated events tied in. const manualImagesData = getImagesWithEvents( manualImages?.data ?? [], - events + imageEvents ); // Automatic images with the associated events tied in. const automaticImagesData = getImagesWithEvents( automaticImages?.data ?? [], - events + imageEvents ); const [drawer, setDrawer] = React.useState( diff --git a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx index c52bed30e60..b2c291da347 100644 --- a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx +++ b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx @@ -27,7 +27,7 @@ import { Tabs } from 'src/components/ReachTabs'; import { SafeTabPanel } from 'src/components/SafeTabPanel/SafeTabPanel'; import { TabLinkList } from 'src/components/TabLinkList/TabLinkList'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { @@ -59,6 +59,8 @@ const CloneLanding = () => { const location = useLocation(); const theme = useTheme(); + const { resetEventsPolling } = usePollingInterval(); + const linodeId = Number(_linodeId); const { data: _configs } = useAllLinodeConfigsQuery(linodeId); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx index c666a2d414f..4c0e9c9075f 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -45,7 +45,6 @@ import { WithQueryClientProps, withQueryClient, } from 'src/containers/withQueryClient.container'; -import { resetEventsPolling } from 'src/eventsPolling'; import withAgreements, { AgreementsProps, } from 'src/features/Account/Agreements/withAgreements'; @@ -69,6 +68,7 @@ import { } from 'src/utilities/formatRegion'; import { isEURegion } from 'src/utilities/formatRegion'; import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; +import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; import { getQueryParamsFromQueryString } from 'src/utilities/queryParams'; import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { validatePassword } from 'src/utilities/validatePassword'; @@ -85,7 +85,7 @@ import type { LinodeTypeClass, PriceObject, } from '@linode/api-v4/lib/linodes'; -import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; +import { WithPollingIntervalProps, withPollingInterval } from 'src/containers/events.container'; const DEFAULT_IMAGE = 'linode/debian11'; @@ -137,7 +137,8 @@ type CombinedProps = WithSnackbarProps & AgreementsProps & WithQueryClientProps & WithMarketplaceAppsProps & - WithAccountSettingsProps; + WithAccountSettingsProps & + WithPollingIntervalProps; const defaultState: State = { assignPublicIPv4Address: false, @@ -866,7 +867,7 @@ class LinodeCreateContainer extends React.PureComponent { ); /** reset the Events polling */ - resetEventsPolling(); + this.props.resetEventsPolling(); // If a VPC was assigned, invalidate the query so that the relevant VPC data // gets displayed in the LinodeEntityDetail @@ -945,7 +946,8 @@ export default recompose( withAgreements, withQueryClient, withAccountSettings, - withMarketplaceApps + withMarketplaceApps, + withPollingInterval )(LinodeCreateContainer); const actionsAndLabels = { diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromImageContent.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromImageContent.test.tsx index 7398340c845..af2bc8d954a 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromImageContent.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromImageContent.test.tsx @@ -3,12 +3,11 @@ import * as React from 'react'; import { Provider } from 'react-redux'; import { LinodeThemeWrapper } from 'src/LinodeThemeWrapper'; -import { queryClientFactory } from 'src/queries/base'; import { storeFactory } from 'src/store'; import { CombinedProps, FromImageContent } from './FromImageContent'; -const store = storeFactory(queryClientFactory()); +const store = storeFactory(); const mockProps: CombinedProps = { accountBackupsEnabled: false, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.test.tsx index a2a1bc48a6c..4299dff10ee 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.test.tsx @@ -4,12 +4,11 @@ import { Provider } from 'react-redux'; import { linodes } from 'src/__data__/linodes'; import { LinodeThemeWrapper } from 'src/LinodeThemeWrapper'; -import { queryClientFactory } from 'src/queries/base'; import { storeFactory } from 'src/store'; import { CombinedProps, FromLinodeContent } from './FromLinodeContent'; -const store = storeFactory(queryClientFactory()); +const store = storeFactory(); const mockProps: CombinedProps = { accountBackupsEnabled: false, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.test.tsx index 68879bb6b47..aedc6d8d18d 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.test.tsx @@ -5,7 +5,6 @@ import { Provider } from 'react-redux'; import { UserDefinedFields as mockUserDefinedFields } from 'src/__data__/UserDefinedFields'; import { LinodeThemeWrapper } from 'src/LinodeThemeWrapper'; import { imageFactory } from 'src/factories/images'; -import { queryClientFactory } from 'src/queries/base'; import { storeFactory } from 'src/store'; import { @@ -13,7 +12,7 @@ import { FromStackScriptContent, } from './FromStackScriptContent'; -const store = storeFactory(queryClientFactory()); +const store = storeFactory(); const mockImages = imageFactory.buildList(10); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx index 9d95a20e899..3924ed56fa5 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useLinodeBackupsCancelMutation } from 'src/queries/linodes/backups'; import { sendBackupsDisabledEvent } from 'src/utilities/analytics'; @@ -24,6 +24,8 @@ export const CancelBackupsDialog = (props: Props) => { mutateAsync: cancelBackups, } = useLinodeBackupsCancelMutation(linodeId); + const { resetEventsPolling } = usePollingInterval(); + const onCancelBackups = async () => { await cancelBackups(); enqueueSnackbar('Backups are being canceled for this Linode', { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx index 26ffee89946..4cf9a1d6007 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx @@ -5,12 +5,12 @@ import * as React from 'react'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; +import { FormControl } from 'src/components/FormControl'; import { Notice } from 'src/components/Notice/Notice'; +import { Paper } from 'src/components/Paper'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { FormControl } from 'src/components/FormControl'; -import { Paper } from 'src/components/Paper'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useLinodeBackupSnapshotMutation } from 'src/queries/linodes/backups'; import { getErrorMap } from 'src/utilities/errorUtils'; @@ -25,6 +25,8 @@ export const CaptureSnapshot = (props: Props) => { const { isReadOnly, linodeId } = props; const { enqueueSnackbar } = useSnackbar(); + const { resetEventsPolling } = usePollingInterval(); + const { error: snapshotError, isLoading: isSnapshotLoading, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx index e30b20a2b97..39307e41b58 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx @@ -7,7 +7,7 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { Currency } from 'src/components/Currency'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useLinodeBackupsEnableMutation } from 'src/queries/linodes/backups'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useTypeQuery } from 'src/queries/types'; @@ -49,6 +49,8 @@ export const EnableBackupsDialog = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); + const { resetEventsPolling } = usePollingInterval(); + const handleEnableBackups = async () => { await enableBackups(); resetEventsPolling(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx index f3f1ca41d98..dfdcd0c680b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx @@ -11,7 +11,7 @@ import { FormControl } from 'src/components/FormControl'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { FormHelperText } from 'src/components/FormHelperText'; import { Notice } from 'src/components/Notice/Notice'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useLinodeBackupRestoreMutation } from 'src/queries/linodes/backups'; import { useAllLinodesQuery, @@ -31,6 +31,8 @@ export const RestoreToLinodeDrawer = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); const { data: linode } = useLinodeQuery(linodeId, open); + const { resetEventsPolling } = usePollingInterval(); + const { data: linodes, error: linodeError, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx index c18bbd66850..109bc10214d 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useRebootLinodeMutation } from 'src/queries/linodes/linodes'; interface Props { @@ -21,6 +21,8 @@ export const BootConfigDialog = (props: Props) => { const { error, isLoading, mutateAsync } = useRebootLinodeMutation(linodeId); + const { resetEventsPolling } = usePollingInterval(); + const onBoot = async () => { await mutateAsync({ config_id: config?.id ?? -1 }); resetEventsPolling(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx index a75a2483307..e46ebc77c24 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx @@ -16,7 +16,6 @@ import { Checkbox } from 'src/components/Checkbox'; import { Divider } from 'src/components/Divider'; import { ImageSelect } from 'src/components/ImageSelect/ImageSelect'; import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; -import { resetEventsPolling } from 'src/eventsPolling'; import { UserDataAccordion } from 'src/features/Linodes/LinodesCreate/UserDataAccordion/UserDataAccordion'; import { regionSupportsMetadata } from 'src/features/Linodes/LinodesCreate/utilities'; import { useFlags } from 'src/hooks/useFlags'; @@ -36,6 +35,7 @@ import { StyledGrid, StyledNotice, } from './RebuildFromImage.styles'; +import { usePollingInterval } from 'src/queries/events'; interface Props { disabled: boolean; @@ -76,6 +76,8 @@ export const RebuildFromImage = (props: Props) => { const { data: preferences } = usePreferences(); + const { resetEventsPolling } = usePollingInterval(); + const { enqueueSnackbar } = useSnackbar(); const flags = useFlags(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx index 987785f7955..9fad4e9ea10 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx @@ -13,7 +13,6 @@ import { AccessPanel } from 'src/components/AccessPanel/AccessPanel'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ImageSelect } from 'src/components/ImageSelect/ImageSelect'; import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; -import { resetEventsPolling } from 'src/eventsPolling'; import { ImageEmptyState } from 'src/features/Linodes/LinodesCreate/TabbedContent/ImageEmptyState'; import SelectStackScriptPanel from 'src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel'; import StackScriptDialog from 'src/features/StackScripts/StackScriptDialog'; @@ -24,6 +23,7 @@ import { } from 'src/features/StackScripts/stackScriptUtils'; import { useStackScript } from 'src/hooks/useStackScript'; import { listToItemsByID } from 'src/queries/base'; +import { usePollingInterval } from 'src/queries/events'; import { useAllImagesQuery } from 'src/queries/images'; import { usePreferences } from 'src/queries/preferences'; import { filterImagesByType } from 'src/store/image/image.helpers'; @@ -70,6 +70,8 @@ export const RebuildFromStackScript = (props: Props) => { const { data: preferences } = usePreferences(); + const { resetEventsPolling } = usePollingInterval(); + const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx index 55ee40a7e73..b012f45c001 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -26,6 +26,8 @@ export const BareMetalRescue = (props: Props) => { linodeId !== undefined && isOpen ); + const { resetEventsPolling } = usePollingInterval(); + React.useEffect(() => { if (isOpen) { setError(undefined); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx index bb36273ccbf..87e3d6386ef 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx @@ -10,8 +10,8 @@ import { Dialog } from 'src/components/Dialog/Dialog'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; -import { resetEventsPolling } from 'src/eventsPolling'; import { usePrevious } from 'src/hooks/usePrevious'; +import { usePollingInterval } from 'src/queries/events'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeQuery, @@ -140,6 +140,8 @@ export const StandardRescueDialog = (props: Props) => { deviceMap ); + const { resetEventsPolling } = usePollingInterval(); + const { enqueueSnackbar } = useSnackbar(); const [APIError, setAPIError] = React.useState(''); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx index 40d3b3a7507..dcb5578e2b1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx @@ -17,7 +17,6 @@ import { Notice } from 'src/components/Notice/Notice'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; import { linodeInTransition } from 'src/features/Linodes/transitions'; import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel'; import { useFlags } from 'src/hooks/useFlags'; @@ -44,6 +43,7 @@ import { import { UnifiedMigrationPanel } from './LinodeResizeUnifiedMigrationPanel'; import type { ButtonProps } from 'src/components/Button/Button'; +import { usePollingInterval } from 'src/queries/events'; interface Props { linodeId?: number; @@ -89,6 +89,8 @@ export const LinodeResize = (props: Props) => { mutateAsync: resizeLinode, } = useLinodeResizeMutation(linodeId ?? -1); + const { resetEventsPolling } = usePollingInterval(); + const { data: regionsData } = useRegionsQuery(); const hostMaintenance = linode?.status === 'stopped'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx index 7378305b1ef..2c81618cf73 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx @@ -6,7 +6,7 @@ import { Button } from 'src/components/Button/Button'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useDeleteLinodeMutation, useLinodeQuery, @@ -26,6 +26,8 @@ export const LinodeSettingsDeletePanel = (props: Props) => { mutateAsync: deleteLinode, } = useDeleteLinodeMutation(linodeId); + const { resetEventsPolling } = usePollingInterval(); + const history = useHistory(); const [open, setOpen] = React.useState(false); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx index 0297f8516c0..070649247b6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx @@ -16,7 +16,6 @@ import { InputAdornment } from 'src/components/InputAdornment'; import { Mode, ModeSelect } from 'src/components/ModeSelect/ModeSelect'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; -import { resetEventsPolling } from 'src/eventsPolling'; import { useAllLinodeDisksQuery, useLinodeDiskCreateMutation, @@ -25,6 +24,7 @@ import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import { ImageAndPassword } from '../LinodeSettings/ImageAndPassword'; +import { usePollingInterval } from 'src/queries/events'; type FileSystem = 'ext3' | 'ext4' | 'initrd' | 'raw' | 'swap'; @@ -51,6 +51,8 @@ export const CreateDiskDrawer = (props: Props) => { const { linodeId, onClose, open } = props; const { enqueueSnackbar } = useSnackbar(); + const { resetEventsPolling } = usePollingInterval(); + const [selectedMode, setSelectedMode] = React.useState('empty'); const { data: linode } = useLinodeQuery(linodeId, open); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx index 9d45b8c3f88..484d5d635b7 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx @@ -8,13 +8,13 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Code } from 'src/components/Code/Code'; import { Drawer } from 'src/components/Drawer'; +import { FormHelperText } from 'src/components/FormHelperText'; +import { InputAdornment } from 'src/components/InputAdornment'; import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { TextTooltip } from 'src/components/TextTooltip'; -import { FormHelperText } from 'src/components/FormHelperText'; -import { InputAdornment } from 'src/components/InputAdornment'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useAllLinodeDisksQuery, useLinodeDiskResizeMutation, @@ -45,6 +45,8 @@ export const ResizeDiskDrawer = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); + const { resetEventsPolling } = usePollingInterval(); + const { mutateAsync: resizeDisk, reset } = useLinodeDiskResizeMutation( linodeId, disk?.id ?? -1 diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx index b96c599cb47..23d681e8675 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; import { MBpsIntraDC } from 'src/constants'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useStartLinodeMutationMutation } from 'src/queries/linodes/actions'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; @@ -24,6 +24,8 @@ export const MutationNotification = (props: Props) => { const { data: linode } = useLinodeQuery(linodeId); + const { resetEventsPolling } = usePollingInterval(); + const { data: currentTypeInfo } = useTypeQuery( linode?.type ?? '', Boolean(linode?.type) diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx index c444f59641c..7c4b71924e2 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx @@ -4,9 +4,9 @@ import { useQueryClient } from 'react-query'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; import { useFlags } from 'src/hooks/useFlags'; import { useAccount } from 'src/queries/account'; +import { usePollingInterval } from 'src/queries/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useDeleteLinodeMutation, @@ -29,6 +29,8 @@ export const DeleteLinodeDialog = (props: Props) => { const flags = useFlags(); const { data: account } = useAccount(); + const { resetEventsPolling } = usePollingInterval(); + const enableVPCActions = isFeatureEnabled( 'VPCs', Boolean(flags.vpc), diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.tsx index 49f314ddc3b..5d37d449092 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodesLanding.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { connect } from 'react-redux'; import { Link, RouteComponentProps, withRouter } from 'react-router-dom'; import { compose } from 'recompose'; @@ -31,7 +30,6 @@ import { LinodeRebuildDialog } from '../LinodesDetail/LinodeRebuild/LinodeRebuil import { RescueDialog } from '../LinodesDetail/LinodeRescue/RescueDialog'; import { LinodeResize } from '../LinodesDetail/LinodeResize/LinodeResize'; import { Action, PowerActionsDialog } from '../PowerActionsDialogOrDrawer'; -import { linodesInTransition as _linodesInTransition } from '../transitions'; import { CardView } from './CardView'; import { DeleteLinodeDialog } from './DeleteLinodeDialog'; import { DisplayGroupedLinodes } from './DisplayGroupedLinodes'; @@ -48,7 +46,6 @@ import { ExtendedStatus, statusToPriority } from './utils'; import type { Config } from '@linode/api-v4/lib/linodes/types'; import type { APIError } from '@linode/api-v4/lib/types'; import type { PreferenceToggleProps } from 'src/components/PreferenceToggle/PreferenceToggle'; -import type { MapState } from 'src/store/types'; interface State { deleteDialogOpen: boolean; @@ -84,15 +81,13 @@ type RouteProps = RouteComponentProps; export interface LinodesLandingProps { LandingHeader?: React.ReactElement; linodesData: LinodeWithMaintenance[]; + linodesInTransition: Set; linodesRequestError?: APIError[]; linodesRequestLoading: boolean; someLinodesHaveScheduledMaintenance: boolean; } -type CombinedProps = LinodesLandingProps & - StateProps & - RouteProps & - WithProfileProps; +type CombinedProps = LinodesLandingProps & RouteProps & WithProfileProps; class ListLinodes extends React.Component { render() { @@ -435,21 +430,8 @@ const sendGroupByAnalytic = (value: boolean) => { sendGroupByTagEnabledEvent(eventCategory, value); }; -interface StateProps { - linodesInTransition: Set; -} - -const mapStateToProps: MapState = (state) => { - return { - linodesInTransition: _linodesInTransition(state.events.events), - }; -}; - -const connected = connect(mapStateToProps, undefined); - export const enhanced = compose( withRouter, - connected, withFeatureFlagConsumer, withProfile ); diff --git a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx index 6503309e32a..7471d12995d 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx @@ -10,7 +10,6 @@ import { Notice } from 'src/components/Notice/Notice'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { MBpsInterDC } from 'src/constants'; -import { resetEventsPolling } from 'src/eventsPolling'; import { EUAgreementCheckbox } from 'src/features/Account/Agreements/EUAgreementCheckbox'; import { regionSupportsMetadata } from 'src/features/Linodes/LinodesCreate/utilities'; import { useFlags } from 'src/hooks/useFlags'; @@ -19,7 +18,7 @@ import { useAccountAgreements, useMutateAccountAgreements, } from 'src/queries/accountAgreements'; -import { useInProgressEvents } from 'src/queries/events'; +import { useInProgressEvents, usePollingInterval } from 'src/queries/events'; import { useImageQuery } from 'src/queries/images'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { @@ -29,7 +28,7 @@ import { import { useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { useTypeQuery } from 'src/queries/types'; -import { isEventRelevantToLinode } from 'src/store/events/event.selectors'; +import { isEventRelevantToLinode } from 'src/store/events/event.helpers'; import { sendMigrationInitiatedEvent } from 'src/utilities/analytics'; import { formatDate } from 'src/utilities/formatDate'; import { getGDPRDetails } from 'src/utilities/formatRegion'; @@ -52,6 +51,8 @@ export const MigrateLinode = React.memo((props: Props) => { const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); + const { resetEventsPolling } = usePollingInterval(); + const { data: linode } = useLinodeQuery( linodeId ?? -1, linodeId !== undefined && open diff --git a/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx b/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx index 59b7d97182a..a5759e64a6b 100644 --- a/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx +++ b/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx @@ -8,7 +8,7 @@ import Select from 'src/components/EnhancedSelect/Select'; import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useBootLinodeMutation, @@ -80,6 +80,8 @@ export const PowerActionsDialog = (props: Props) => { mutateAsync: shutdownLinode, } = useShutdownLinodeMutation(linodeId ?? -1); + const { resetEventsPolling } = usePollingInterval(); + const [selectedConfigID, setSelectConfigID] = React.useState( null ); diff --git a/packages/manager/src/features/Linodes/index.tsx b/packages/manager/src/features/Linodes/index.tsx index 4baee4c06ea..990a40f12d6 100644 --- a/packages/manager/src/features/Linodes/index.tsx +++ b/packages/manager/src/features/Linodes/index.tsx @@ -3,8 +3,10 @@ import { Redirect, Route, Switch } from 'react-router-dom'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; import { useAllAccountMaintenanceQuery } from 'src/queries/accountMaintenance'; +import { useInProgressEvents } from 'src/queries/events'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { addMaintenanceToLinodes } from 'src/utilities/linodes'; +import { linodesInTransition } from './transitions'; const LinodesLanding = React.lazy( () => import('./LinodesLanding/LinodesLanding') @@ -46,6 +48,8 @@ const LinodesLandingWrapper: React.FC = React.memo(() => { (thisAccountMaintenance) => thisAccountMaintenance.entity.type === 'linode' ); + const { data: events } = useInProgressEvents(); + const linodesData = addMaintenanceToLinodes( accountMaintenanceData ?? [], linodes ?? [] @@ -57,6 +61,7 @@ const LinodesLandingWrapper: React.FC = React.memo(() => { someLinodesHaveScheduledMaintenance )} linodesData={linodesData} + linodesInTransition={linodesInTransition(events ?? [])} linodesRequestError={error ?? undefined} linodesRequestLoading={isLoading} /> diff --git a/packages/manager/src/features/Linodes/transitions.ts b/packages/manager/src/features/Linodes/transitions.ts index 853f7fb2ca7..5013fe65c8e 100644 --- a/packages/manager/src/features/Linodes/transitions.ts +++ b/packages/manager/src/features/Linodes/transitions.ts @@ -1,12 +1,11 @@ import { Event, EventAction } from '@linode/api-v4/lib/account'; -import { isInProgressEvent } from 'src/store/events/event.helpers'; import { isEventRelevantToLinode, + isInProgressEvent, isPrimaryEntity, isSecondaryEntity, -} from 'src/store/events/event.selectors'; -import { ExtendedEvent } from 'src/store/events/event.types'; +} from 'src/store/events/event.helpers'; import { capitalizeAllWords } from 'src/utilities/capitalize'; export const transitionStatus = [ @@ -96,10 +95,8 @@ export const linodesInTransition = (events: Event[]) => { // an in-progress event, but we don't have the updated status from the API yet. // In this case it doesn't have a recentEvent attached (since it has completed), // but its status is still briefly in transition, so give it a progress of 100. -export const getProgressOrDefault = ( - event?: ExtendedEvent, - defaultProgress = 0 -) => event?.percent_complete ?? defaultProgress; +export const getProgressOrDefault = (event?: Event, defaultProgress = 0) => + event?.percent_complete ?? defaultProgress; // Linodes have a literal "status" given by the API (linode.status). There are // states the Linode can be in which aren't entirely communicated with the diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx b/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx index 2ca94bd5524..1dc3769250d 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { useEventsInfiniteQuery } from 'src/queries/events'; import { isInProgressEvent } from 'src/store/events/event.helpers'; -import { ExtendedEvent } from 'src/store/events/event.types'; import { removeBlocklistedEvents } from 'src/utilities/eventUtils'; import { notificationContext as _notificationContext } from '../NotificationContext'; @@ -21,7 +20,7 @@ const unwantedEvents: EventAction[] = [ 'volume_update', ]; -export const useEventNotifications = (givenEvents?: ExtendedEvent[]) => { +export const useEventNotifications = (givenEvents?: Event[]) => { const events = removeBlocklistedEvents( givenEvents ?? useEventsInfiniteQuery().events ); @@ -48,19 +47,21 @@ export const useEventNotifications = (givenEvents?: ExtendedEvent[]) => { }; const formatEventForDisplay = ( - event: ExtendedEvent, + event: Event, onClose: () => void ): NotificationItem => ({ body: , countInTotal: !event.seen, + eventId: event.id, id: `event-${event.id}`, }); const formatProgressEventForDisplay = ( - event: ExtendedEvent, + event: Event, onClose: () => void ): NotificationItem => ({ body: , countInTotal: !event.seen, + eventId: event.id, id: `progress-event-${event.id}`, }); diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/useFormattedNotifications.tsx b/packages/manager/src/features/NotificationCenter/NotificationData/useFormattedNotifications.tsx index 1b1aa6c94ed..32b53185ebf 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/useFormattedNotifications.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationData/useFormattedNotifications.tsx @@ -366,6 +366,7 @@ const formatNotificationForDisplay = ( ): NotificationItem => ({ body: , countInTotal: shouldIncludeInCount, + eventId: -1, id: `notification-${idx}`, }); diff --git a/packages/manager/src/features/NotificationCenter/NotificationSection.tsx b/packages/manager/src/features/NotificationCenter/NotificationSection.tsx index 621dc5655e5..fea83f22ac9 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationSection.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationSection.tsx @@ -41,6 +41,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ export interface NotificationItem { body: JSX.Element | string; countInTotal: boolean; + eventId: number; id: string; } diff --git a/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx b/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx index 447c2ce18e5..05d8752a001 100644 --- a/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx +++ b/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx @@ -21,7 +21,7 @@ import Notifications from 'src/features/NotificationCenter/Notifications'; import { useDismissibleNotifications } from 'src/hooks/useDismissibleNotifications'; import { usePrevious } from 'src/hooks/usePrevious'; import { useNotificationsQuery } from 'src/queries/accountNotifications'; -import { markAllSeen } from 'src/store/events/event.request'; +import { useMarkEventsAsSeen } from 'src/queries/events'; import { ThunkDispatch } from 'src/store/types'; import { omittedProps } from 'src/utilities/omittedProps'; @@ -59,6 +59,7 @@ export const NotificationMenu = () => { const formattedNotifications = useFormattedNotifications(); const eventNotifications = useEventNotifications(); const notificationContext = React.useContext(_notificationContext); + const { mutateAsync: markEventsAsSeen } = useMarkEventsAsSeen(); const numNotifications = eventNotifications.filter((thisEvent) => thisEvent.countInTotal).length + @@ -98,12 +99,15 @@ export const NotificationMenu = () => { React.useEffect(() => { if (prevOpen && !notificationContext.menuOpen) { // Dismiss seen notifications after the menu has closed. - dispatch(markAllSeen()); + if (eventNotifications.length > 0) { + markEventsAsSeen(eventNotifications[0].eventId); + } dismissNotifications(notifications ?? [], { prefix: 'notificationMenu' }); } }, [ notificationContext.menuOpen, dismissNotifications, + eventNotifications, notifications, dispatch, prevOpen, diff --git a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx index 8957a0d29c8..9b0eb6df983 100644 --- a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx @@ -10,12 +10,12 @@ import Select, { Item } from 'src/components/EnhancedSelect'; import { FormControl } from 'src/components/FormControl'; import { FormHelperText } from 'src/components/FormHelperText'; import { Notice } from 'src/components/Notice/Notice'; -import { resetEventsPolling } from 'src/eventsPolling'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useGrants } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; +import { usePollingInterval } from 'src/queries/events'; interface Props { onClose: () => void; @@ -37,6 +37,8 @@ export const AttachVolumeDrawer = React.memo((props: Props) => { const { enqueueSnackbar } = useSnackbar(); + const { resetEventsPolling } = usePollingInterval(); + const { data: grants } = useGrants(); const { error, mutateAsync: attachVolume } = useAttachVolumeMutation(); diff --git a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx b/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx index 1c653956e58..c8e33406004 100644 --- a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx @@ -8,7 +8,7 @@ import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useGrants } from 'src/queries/profile'; import { useCloneVolumeMutation } from 'src/queries/volumes'; import { @@ -31,6 +31,8 @@ export const CloneVolumeDrawer = (props: Props) => { const { mutateAsync: cloneVolume } = useCloneVolumeMutation(); + const { resetEventsPolling } = usePollingInterval(); + const { data: grants } = useGrants(); // Even if a restricted user has the ability to create Volumes, they diff --git a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx b/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx index dfe66eafe19..fe1eeb13518 100644 --- a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useDeleteVolumeMutation } from 'src/queries/volumes'; interface Props { @@ -21,6 +21,8 @@ export const DeleteVolumeDialog = (props: Props) => { mutateAsync: deleteVolume, } = useDeleteVolumeMutation(); + const { resetEventsPolling } = usePollingInterval(); + const onDelete = () => { deleteVolume({ id: volume?.id ?? -1 }).then(() => { onClose(); diff --git a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx index 2a6359eb6b5..aac6d40c1d3 100644 --- a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Typography } from 'src/components/Typography'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useDetachVolumeMutation } from 'src/queries/volumes'; @@ -20,6 +20,8 @@ export const DetachVolumeDialog = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); + const { resetEventsPolling } = usePollingInterval(); + const { data: linode } = useLinodeQuery( volume?.linode_id ?? -1, volume?.linode_id !== null && volume?.linode_id !== undefined diff --git a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx b/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx index 7b587d57f6c..7eec1f3d030 100644 --- a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; -import { resetEventsPolling } from 'src/eventsPolling'; import { useGrants } from 'src/queries/profile'; import { useResizeVolumeMutation } from 'src/queries/volumes'; import { @@ -17,6 +16,7 @@ import { import { PricePanel } from './VolumeDrawer/PricePanel'; import { SizeField } from './VolumeDrawer/SizeField'; +import { usePollingInterval } from 'src/queries/events'; interface Props { onClose: () => void; @@ -29,6 +29,8 @@ export const ResizeVolumeDrawer = (props: Props) => { const { mutateAsync: resizeVolume } = useResizeVolumeMutation(); + const { resetEventsPolling } = usePollingInterval(); + const validationSchema = ResizeVolumeSchema(volume?.size ?? -1); const { enqueueSnackbar } = useSnackbar(); diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx index 1a7b3df774e..9e53874fc98 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx @@ -7,7 +7,7 @@ import { number, object } from 'yup'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Notice } from 'src/components/Notice/Notice'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useGrants } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; import { @@ -46,6 +46,8 @@ export const LinodeVolumeAttachForm = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); + const { resetEventsPolling } = usePollingInterval(); + const linodeGrant = grants?.linode.find( (grant: Grant) => grant.id === linode.id ); diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx index 788f3aee40d..16973a7bb80 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx @@ -10,7 +10,7 @@ import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; import { MAX_VOLUME_SIZE } from 'src/constants'; -import { resetEventsPolling } from 'src/eventsPolling'; +import { usePollingInterval } from 'src/queries/events'; import { useGrants, useProfile } from 'src/queries/profile'; import { useCreateVolumeMutation } from 'src/queries/volumes'; import { sendCreateVolumeEvent } from 'src/utilities/analytics'; @@ -57,6 +57,8 @@ export const LinodeVolumeCreateForm = (props: Props) => { const { data: grants } = useGrants(); const { mutateAsync: createVolume } = useCreateVolumeMutation(); + const { resetEventsPolling } = usePollingInterval(); + const disabled = profile?.restricted && !grants?.global.add_volumes; const { diff --git a/packages/manager/src/hooks/useToastNotifications.tsx b/packages/manager/src/hooks/useToastNotifications.tsx index 91de38ca5d0..684bd6016d5 100644 --- a/packages/manager/src/hooks/useToastNotifications.tsx +++ b/packages/manager/src/hooks/useToastNotifications.tsx @@ -1,7 +1,6 @@ import { Event, EventAction } from '@linode/api-v4/lib/account/types'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import 'rxjs/add/operator/filter'; import { Link } from 'src/components/Link'; import { SupportLink } from 'src/components/SupportLink'; diff --git a/packages/manager/src/index.tsx b/packages/manager/src/index.tsx index ed4c4c683cb..d3fee1824f2 100644 --- a/packages/manager/src/index.tsx +++ b/packages/manager/src/index.tsx @@ -23,7 +23,7 @@ import './index.css'; import { queryClientFactory } from './queries/base'; const queryClient = queryClientFactory(); -const store = storeFactory(queryClient); +const store = storeFactory(); setupInterceptors(store); diff --git a/packages/manager/src/queries/accountOAuth.ts b/packages/manager/src/queries/accountOAuth.ts index 755547104d2..23277144230 100644 --- a/packages/manager/src/queries/accountOAuth.ts +++ b/packages/manager/src/queries/accountOAuth.ts @@ -12,7 +12,7 @@ import { } from '@linode/api-v4'; import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { EventWithStore } from 'src/events'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; import { queryKey as accountQueryKey } from './account'; @@ -66,7 +66,7 @@ export const useUpdateOAuthClientMutation = (id: string) => { ); }; -export const oauthClientsEventHandler = ({ queryClient }: EventWithStore) => { +export const oauthClientsEventHandler = ({ queryClient }: EventHandlerData) => { // We may over-fetch because on `onSuccess` also invalidates, but this will be // good for UX because Cloud will always be up to date queryClient.invalidateQueries(queryKey); diff --git a/packages/manager/src/queries/databases.ts b/packages/manager/src/queries/databases.ts index 167117a62c7..a17e46f3daf 100644 --- a/packages/manager/src/queries/databases.ts +++ b/packages/manager/src/queries/databases.ts @@ -37,7 +37,7 @@ import { useQueryClient, } from 'react-query'; -import { EventWithStore } from 'src/events'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; import { getAll } from 'src/utilities/getAll'; import { queryPresets, updateInPaginatedStore } from './base'; @@ -211,7 +211,7 @@ export const useRestoreFromBackupMutation = ( ); }; -export const databaseEventsHandler = (event: EventWithStore) => { +export const databaseEventsHandler = (event: EventHandlerData) => { const { event: { action, entity, status }, queryClient, diff --git a/packages/manager/src/queries/domains.ts b/packages/manager/src/queries/domains.ts index 674e62a0f4b..fe57b612bfc 100644 --- a/packages/manager/src/queries/domains.ts +++ b/packages/manager/src/queries/domains.ts @@ -22,10 +22,10 @@ import { } from '@linode/api-v4/lib/types'; import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { EventWithStore } from 'src/events'; import { getAll } from 'src/utilities/getAll'; import { queryKey as PROFILE_QUERY_KEY } from './profile'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; export const queryKey = 'domains'; @@ -117,7 +117,7 @@ export const useUpdateDomainMutation = () => { ); }; -export const domainEventsHandler = ({ queryClient }: EventWithStore) => { +export const domainEventsHandler = ({ queryClient }: EventHandlerData) => { // Invalidation is agressive beacuse it will invalidate on every domain event, but // it is worth it for the UX benefits. We can fine tune this later if we need to. queryClient.invalidateQueries([queryKey]); diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 7f3cb1e9941..a9dc3d9008a 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -1,7 +1,8 @@ -import { getEvents } from '@linode/api-v4'; +import { getEvents, markEventSeen } from '@linode/api-v4'; import { InfiniteData, useInfiniteQuery, + useMutation, useQuery, useQueryClient, } from 'react-query'; @@ -125,7 +126,7 @@ export const useEventsPoller = () => { }); }; -const usePollingInterval = () => { +export const usePollingInterval = () => { const queryKey = ['events', 'interval']; const queryClient = useQueryClient(); const { data: intervalMultiplier = 1 } = useQuery(queryKey, () => @@ -140,6 +141,47 @@ const usePollingInterval = () => { pollingInterval: DISABLE_EVENT_THROTTLE ? 500 : intervalMultiplier * INTERVAL, - resetPollingInterval: () => queryClient.setQueryData(queryKey, 1), + resetEventsPolling: () => queryClient.setQueryData(queryKey, 1), }; }; + +export const useMarkEventsAsSeen = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, APIError[], number>( + (eventId) => markEventSeen(eventId), + { + onSuccess: (_, eventId) => { + queryClient.setQueryData>>( + ['events', 'infinite', {}], + (prev) => { + if (!prev) { + return { + pageParams: [], + pages: [], + }; + } + + let foundLatestSeenEvent = false; + + for (const page of prev.pages) { + for (const event of page.data) { + if (event.id === eventId) { + foundLatestSeenEvent = true; + } + if (foundLatestSeenEvent) { + event.seen = true; + } + } + } + + return { + pageParams: prev?.pageParams ?? [], + pages: prev?.pages ?? [], + }; + } + ); + }, + } + ); +}; diff --git a/packages/manager/src/queries/firewalls.ts b/packages/manager/src/queries/firewalls.ts index 37c3324a011..2cb17838ee0 100644 --- a/packages/manager/src/queries/firewalls.ts +++ b/packages/manager/src/queries/firewalls.ts @@ -22,7 +22,7 @@ import { } from '@linode/api-v4/lib/types'; import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { EventWithStore } from 'src/events'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; import { queryKey as linodesQueryKey } from 'src/queries/linodes/linodes'; import { getAll } from 'src/utilities/getAll'; @@ -188,7 +188,7 @@ const getAllFirewallsRequest = () => getFirewalls(passedParams, passedFilter) )().then((data) => data.data); -export const firewallEventsHandler = ({ queryClient }: EventWithStore) => { +export const firewallEventsHandler = ({ queryClient }: EventHandlerData) => { // We will over-fetch a little bit, bit this ensures Cloud firewalls are *always* up to date queryClient.invalidateQueries([queryKey]); }; diff --git a/packages/manager/src/queries/images.ts b/packages/manager/src/queries/images.ts index ba62c58eea3..28cd159874f 100644 --- a/packages/manager/src/queries/images.ts +++ b/packages/manager/src/queries/images.ts @@ -23,11 +23,11 @@ import { useQueryClient, } from 'react-query'; -import { EventWithStore } from 'src/events'; import { getAll } from 'src/utilities/getAll'; import { doesItemExistInPaginatedStore, updateInPaginatedStore } from './base'; import { queryKey as PROFILE_QUERY_KEY } from './profile'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; export const queryKey = 'images'; @@ -126,7 +126,7 @@ export const useAllImagesQuery = ( export const useUploadImageQuery = (payload: ImageUploadPayload) => useMutation(() => uploadImage(payload)); -export const imageEventsHandler = ({ event, queryClient }: EventWithStore) => { +export const imageEventsHandler = ({ event, queryClient }: EventHandlerData) => { const { action, entity, status } = event; // Keep the getAll query up to date so that when we have to use it, it contains accurate data diff --git a/packages/manager/src/queries/linodes/events.ts b/packages/manager/src/queries/linodes/events.ts index 427e7041bf5..9e6a3010d95 100644 --- a/packages/manager/src/queries/linodes/events.ts +++ b/packages/manager/src/queries/linodes/events.ts @@ -1,4 +1,4 @@ -import { EventWithStore } from 'src/events'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; import { queryKey as accountNotificationsQueryKey } from 'src/queries/accountNotifications'; import { queryKey as firewallsQueryKey } from 'src/queries/firewalls'; import { queryKey as volumesQueryKey } from 'src/queries/volumes'; @@ -13,7 +13,10 @@ import type { Event } from '@linode/api-v4'; * This event handler runs for any event prefixed with "linode". * For example, "linode_create", "linode_boot", "linode_resize", ... */ -export const linodeEventsHandler = ({ event, queryClient }: EventWithStore) => { +export const linodeEventsHandler = ({ + event, + queryClient, +}: EventHandlerData) => { const linodeId = event.entity?.id; // Early return to cut down the number of invalidations. @@ -105,7 +108,7 @@ export const linodeEventsHandler = ({ event, queryClient }: EventWithStore) => { * Disks have their own handler beacuse the actions are not prefixed with "linode_". * They are prefixed with "disk_". For example "disk_create" or "disk_delete". */ -export const diskEventHandler = ({ event, queryClient }: EventWithStore) => { +export const diskEventHandler = ({ event, queryClient }: EventHandlerData) => { const linodeId = event.entity?.id; if (!linodeId || ['scheduled', 'started'].includes(event.status)) { diff --git a/packages/manager/src/queries/nodebalancers.ts b/packages/manager/src/queries/nodebalancers.ts index 089a818abef..fc38fcf7483 100644 --- a/packages/manager/src/queries/nodebalancers.ts +++ b/packages/manager/src/queries/nodebalancers.ts @@ -29,12 +29,12 @@ import { useQueryClient, } from 'react-query'; -import { EventWithStore } from 'src/events'; import { parseAPIDate } from 'src/utilities/date'; import { getAll } from 'src/utilities/getAll'; import { itemInListCreationHandler, itemInListMutationHandler } from './base'; import { queryKey as PROFILE_QUERY_KEY } from './profile'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; export const queryKey = 'nodebalancers'; @@ -203,7 +203,7 @@ export const useInfiniteNodebalancersQuery = (filter: Filter) => export const nodebalanacerEventHandler = ({ event, queryClient, -}: EventWithStore) => { +}: EventHandlerData) => { if (event.action.startsWith('nodebalancer_config')) { queryClient.invalidateQueries([ queryKey, diff --git a/packages/manager/src/queries/profile.ts b/packages/manager/src/queries/profile.ts index 18e89ff4139..6d316adc161 100644 --- a/packages/manager/src/queries/profile.ts +++ b/packages/manager/src/queries/profile.ts @@ -31,11 +31,10 @@ import { useQueryClient, } from 'react-query'; -import { EventWithStore } from 'src/events'; - import { Grants } from '../../../api-v4/lib'; import { queryKey as accountQueryKey } from './account'; import { queryPresets } from './base'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; export const queryKey = 'profile'; @@ -148,7 +147,7 @@ export const useDeleteSSHKeyMutation = (id: number) => { }); }; -export const sshKeyEventHandler = (event: EventWithStore) => { +export const sshKeyEventHandler = (event: EventHandlerData) => { // This event handler is a bit agressive and will over-fetch, but UX will // be great because this will ensure Cloud has up to date data all the time. diff --git a/packages/manager/src/queries/support.ts b/packages/manager/src/queries/support.ts index a43bf940cf6..60aac525eef 100644 --- a/packages/manager/src/queries/support.ts +++ b/packages/manager/src/queries/support.ts @@ -15,7 +15,7 @@ import { useQueryClient, } from 'react-query'; -import { EventWithStore } from 'src/events'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; import type { APIError, @@ -70,6 +70,8 @@ export const useSupportTicketCloseMutation = (id: number) => { }); }; -export const supportTicketEventHandler = ({ queryClient }: EventWithStore) => { +export const supportTicketEventHandler = ({ + queryClient, +}: EventHandlerData) => { queryClient.invalidateQueries([queryKey]); }; diff --git a/packages/manager/src/queries/tokens.ts b/packages/manager/src/queries/tokens.ts index ce9f6d952b4..598204c72c7 100644 --- a/packages/manager/src/queries/tokens.ts +++ b/packages/manager/src/queries/tokens.ts @@ -15,10 +15,9 @@ import { } from '@linode/api-v4/lib/types'; import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { EventWithStore } from 'src/events'; - import { updateInPaginatedStore } from './base'; import { queryKey } from './profile'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; export const useAppTokensQuery = (params?: Params, filter?: Filter) => { return useQuery, APIError[]>({ @@ -95,6 +94,6 @@ export const useRevokeAppAccessTokenMutation = (id: number) => { }); }; -export function tokenEventHandler({ queryClient }: EventWithStore) { +export function tokenEventHandler({ queryClient }: EventHandlerData) { queryClient.invalidateQueries([queryKey, 'personal-access-tokens']); } diff --git a/packages/manager/src/queries/volumes.ts b/packages/manager/src/queries/volumes.ts index 9b237de9b6e..bce51491ada 100644 --- a/packages/manager/src/queries/volumes.ts +++ b/packages/manager/src/queries/volumes.ts @@ -24,11 +24,11 @@ import { useQueryClient, } from 'react-query'; -import { EventWithStore } from 'src/events'; import { getAll } from 'src/utilities/getAll'; import { updateInPaginatedStore } from './base'; import { queryKey as PROFILE_QUERY_KEY } from './profile'; +import { EventHandlerData } from 'src/hooks/useEventHandlers'; export const queryKey = 'volumes'; @@ -177,7 +177,10 @@ export const useAttachVolumeMutation = () => { export const useDetachVolumeMutation = () => useMutation<{}, APIError[], { id: number }>(({ id }) => detachVolume(id)); -export const volumeEventsHandler = ({ event, queryClient }: EventWithStore) => { +export const volumeEventsHandler = ({ + event, + queryClient, +}: EventHandlerData) => { if (['failed', 'finished', 'notification'].includes(event.status)) { queryClient.invalidateQueries([queryKey]); } diff --git a/packages/manager/src/request.test.tsx b/packages/manager/src/request.test.tsx index d109999eaa1..895f0fc73b6 100644 --- a/packages/manager/src/request.test.tsx +++ b/packages/manager/src/request.test.tsx @@ -3,7 +3,6 @@ import { AxiosError, AxiosHeaders, AxiosResponse } from 'axios'; import { handleStartSession } from 'src/store/authentication/authentication.actions'; import { profileFactory } from './factories'; -import { queryClientFactory } from './queries/base'; import { LinodeError, getURL, @@ -14,7 +13,7 @@ import { storeFactory } from './store'; import type { APIError } from '@linode/api-v4'; -const store = storeFactory(queryClientFactory()); +const store = storeFactory(); const mockAxiosError = { isAxiosError: true, diff --git a/packages/manager/src/store/authentication/authentication.test.ts b/packages/manager/src/store/authentication/authentication.test.ts index 047c9a9c40c..d471873ba65 100644 --- a/packages/manager/src/store/authentication/authentication.test.ts +++ b/packages/manager/src/store/authentication/authentication.test.ts @@ -1,4 +1,3 @@ -import { queryClientFactory as queryClientFactory } from 'src/queries/base'; import { authentication } from 'src/utilities/storage'; import { storeFactory } from '..'; @@ -8,7 +7,7 @@ import { handleStartSession, } from './authentication.actions'; -const store = storeFactory(queryClientFactory()); +const store = storeFactory(); describe('Authentication', () => { authentication.expire.set('hello world'); diff --git a/packages/manager/src/store/events/event.actions.ts b/packages/manager/src/store/events/event.actions.ts deleted file mode 100644 index 33466fb5df2..00000000000 --- a/packages/manager/src/store/events/event.actions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import actionCreatorFactory from 'typescript-fsa'; - -import { ExtendedEvent } from './event.types'; - -type Event = ExtendedEvent; - -export const ADD_EVENTS = `ADD_EVENTS`; - -export const UPDATE_EVENTS_AS_SEEN = `UPDATE_EVENTS_AS_SEEN`; - -export const actionCreator = actionCreatorFactory(`@@manager/events`); - -export const addEvents = actionCreator(ADD_EVENTS); - -export const updateEventsAsSeen = actionCreator(UPDATE_EVENTS_AS_SEEN); - -export const setPollingInterval = actionCreator('set-polling-interval'); - -export const setRequestDeadline = actionCreator('set-request-deadline'); diff --git a/packages/manager/src/store/events/event.helpers.test.ts b/packages/manager/src/store/events/event.helpers.test.ts index 306a071ab84..7918328a4a0 100644 --- a/packages/manager/src/store/events/event.helpers.test.ts +++ b/packages/manager/src/store/events/event.helpers.test.ts @@ -1,19 +1,22 @@ import { Event } from '@linode/api-v4/lib/account'; import { DateTime } from 'luxon'; -import { eventFactory } from 'src/factories/events'; +import { entityFactory, eventFactory } from 'src/factories/events'; import { addToEvents, findInEvents, getNumUnseenEvents, isCompletedEvent, + isEventRelevantToLinode, + isEventRelevantToLinodeAsSecondaryEntity, isInProgressEvent, + isPrimaryEntity, + isSecondaryEntity, mostRecentCreated, setDeletedEvents, updateInProgressEvents, } from './event.helpers'; -import { ExtendedEvent } from './event.types'; describe('event.helpers', () => { describe('findInEvents', () => { @@ -146,9 +149,8 @@ describe('event.helpers', () => { }, ]; - const expected: ExtendedEvent[] = [ + const expected: Event[] = [ { - _deleted: '2018-12-02T23:15:45', action: 'linode_delete', created: '2018-12-02T23:15:45', duration: 0, @@ -170,7 +172,6 @@ describe('event.helpers', () => { username: 'test', }, { - _deleted: '2018-12-02T23:15:45', action: 'linode_boot', created: '2018-12-02T22:55:43', duration: 0, @@ -349,3 +350,68 @@ describe('event.helpers', () => { }); }); }); + +describe('event selector helpers', () => { + describe('isEventRelevantToLinode', () => { + const event0 = eventFactory.build({ + action: 'linode_create', + entity: entityFactory.build({ id: 0 }), + }); + const event1 = eventFactory.build({ + action: 'linode_create', + entity: entityFactory.build({ id: 1 }), + }); + const event2 = eventFactory.build({ + action: 'linode_create', + entity: entityFactory.build({ id: 100 }), + secondary_entity: entityFactory.build({ id: 1 }), + }); + const event3 = eventFactory.build({ + action: 'linode_clone', + secondary_entity: entityFactory.build({ id: 1 }), + }); + it("returns `true` when the linodeId is the event's entity ID, or if it's the event's secondary_entity ID and the event is relevant", () => { + expect(isEventRelevantToLinode(event0, 0)).toBe(true); + expect(isEventRelevantToLinode(event1, 0)).toBe(false); + expect(isEventRelevantToLinode(event2, 1)).toBe(false); + expect(isEventRelevantToLinode(event3, 1)).toBe(true); + }); + }); + + describe('isPrimaryEntity', () => { + const event = eventFactory.build({ + entity: entityFactory.build({ id: 1 }), + }); + it("returns `true` when the linodeId matches the event's entity ID", () => { + expect(isPrimaryEntity(event, 0)).toBe(false); + expect(isPrimaryEntity(event, 1)).toBe(true); + }); + }); + + describe('isSecondaryEntity', () => { + const event = eventFactory.build({ + secondary_entity: entityFactory.build({ id: 1 }), + }); + it("returns `true` when the linodeId matches the event's secondary_entity ID", () => { + expect(isSecondaryEntity(event, 0)).toBe(false); + expect(isSecondaryEntity(event, 1)).toBe(true); + }); + }); + + describe('isEventRelevantToLinodeAsSecondaryEntity', () => { + const linodeCreateEvent = eventFactory.build({ + action: 'linode_create', + }); + const linodeCloneEvent = eventFactory.build({ + action: 'linode_clone', + }); + it('returns `true` if the event type is relevant to Linodes as secondary entities', () => { + expect(isEventRelevantToLinodeAsSecondaryEntity(linodeCreateEvent)).toBe( + false + ); + expect(isEventRelevantToLinodeAsSecondaryEntity(linodeCloneEvent)).toBe( + true + ); + }); + }); +}); diff --git a/packages/manager/src/store/events/event.helpers.ts b/packages/manager/src/store/events/event.helpers.ts index ac2776f5796..fdd23c97495 100644 --- a/packages/manager/src/store/events/event.helpers.ts +++ b/packages/manager/src/store/events/event.helpers.ts @@ -9,8 +9,6 @@ import { compose, equals, findIndex, omit, take, update } from 'ramda'; import { parseAPIDate } from 'src/utilities/date'; import { updateRight } from 'src/utilities/updateRight'; -import { EntityEvent, ExtendedEvent } from './event.types'; - /** We use the epoch on our initial request to get all of the users events. */ export const epoch = new Date(`1970-01-01T00:00:00.000`).getTime(); @@ -51,7 +49,7 @@ export const isRelevantDeletionEvent = ( * entity {null | Entity} */ export const findInEvents = ( - events: Pick[], + events: Pick[], entity: Partial | null = {} ) => findIndex((e) => equals(e.entity, entity), events); @@ -177,8 +175,6 @@ export const isCompletedEvent = ({ }: Pick) => percent_complete !== null && percent_complete === 100; -export const isEntityEvent = (e: Event): e is EntityEvent => Boolean(e.entity); - export const isEventInProgressDiskImagize = (event: Event): boolean => { return ( event.action === 'disk_imagize' && @@ -220,3 +216,33 @@ export const updateInProgressEvents = ( export const getNumUnseenEvents = (events: Pick[]) => events.reduce((result, event) => (event.seen ? result : result + 1), 0); + +export const eventsForLinode = (events: Event[], linodeId: number) => { + return events.filter((event) => isEventRelevantToLinode(event, linodeId)); +}; + +export const isEventRelevantToLinode = (event: Event, linodeId: number) => + isPrimaryEntity(event, linodeId) || + (isSecondaryEntity(event, linodeId) && + isEventRelevantToLinodeAsSecondaryEntity(event)); + +export const isPrimaryEntity = (event: Event, linodeId: number) => + event?.entity?.type === 'linode' && event?.entity?.id === linodeId; + +export const isSecondaryEntity = (event: Event, linodeId: number) => + event?.secondary_entity?.type === 'linode' && + event?.secondary_entity?.id === linodeId; + +// Some event types include a Linode as a `secondary_entity`. A subset of these +// events should be included in the `eventsForLinode` selector since they are +// relevant to that Linode. +// +// An example: `clone_linode` events include the source Linode as the `entity` +// and the target Linode as the `secondary_entity`. In this case, we want the +// consumer of the `eventsForLinode` selector to have access to these events so +// it can do things like display progress bars. +export const eventActionsForLinodeAsSecondaryEntity: EventAction[] = [ + 'linode_clone', +]; +export const isEventRelevantToLinodeAsSecondaryEntity = (event: Event) => + eventActionsForLinodeAsSecondaryEntity.includes(event?.action) \ No newline at end of file diff --git a/packages/manager/src/store/events/event.reducer.test.ts b/packages/manager/src/store/events/event.reducer.test.ts deleted file mode 100644 index e5df7301fbb..00000000000 --- a/packages/manager/src/store/events/event.reducer.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Event } from '@linode/api-v4/lib/account'; -import { DateTime } from 'luxon'; - -import { addEvents } from './event.actions'; -import reducer, { defaultState } from './event.reducer'; - -describe('events.reducer', () => { - describe('reducer', () => { - describe('ADD_EVENTS', () => { - describe('with no events', () => { - it('should return the initial state', () => { - const action = addEvents([]); - const state = reducer(defaultState, action); - expect(state).toEqual(defaultState); - }); - }); - - describe('with events', () => { - const events: Event[] = [ - { - action: 'linode_reboot', - created: '2018-12-03T22:34:09', - duration: 0, - entity: { - id: 11241778, - label: 'node-server', - type: 'linode', - url: '/v4/linode/instances/11241778', - }, - id: 18029572, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-03T19:59:53', - duration: 0, - entity: { - id: 11642886, - label: 'linode11642886', - type: 'linode', - url: '/v4/linode/instances/11642886', - }, - id: 18022171, - message: null, - percent_complete: 80, - rate: null, - read: false, - secondary_entity: null, - seen: false, - status: 'started', - time_remaining: null, - username: 'test', - }, - ]; - const action = addEvents(events); - const state = reducer(defaultState, action); - - it('should add the events to the store.', () => { - expect(state).toHaveProperty('events', events); - }); - - it('should update the mostRecentEventTime', () => { - expect(state).toHaveProperty( - 'mostRecentEventTime', - DateTime.fromISO('2018-12-03T22:34:09', { zone: 'utc' }).valueOf() - ); - }); - - it('should update the countUnseenEvents', () => { - expect(state).toHaveProperty('countUnseenEvents', 1); - }); - - it('should update the inProgressEvents', () => { - expect(state).toHaveProperty('inProgressEvents', { 18022171: 80 }); - }); - }); - }); - }); -}); diff --git a/packages/manager/src/store/events/event.reducer.ts b/packages/manager/src/store/events/event.reducer.ts deleted file mode 100644 index bfb0705d011..00000000000 --- a/packages/manager/src/store/events/event.reducer.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { AnyAction, Reducer } from 'redux'; -import { isType } from 'typescript-fsa'; - -import { - addEvents, - setPollingInterval, - setRequestDeadline, - updateEventsAsSeen, -} from './event.actions'; -import { - epoch, - getNumUnseenEvents, - mostRecentCreated, - updateEvents, - updateInProgressEvents, -} from './event.helpers'; -import { ExtendedEvent } from './event.types'; - -export interface State { - countUnseenEvents: number; - events: ExtendedEvent[]; - inProgressEvents: Record; - mostRecentEventTime: number; - pollingInterval: number; - requestDeadline: number; -} - -export const defaultState: State = { - countUnseenEvents: 0, - events: [], - inProgressEvents: {}, - mostRecentEventTime: epoch, - pollingInterval: 1, - requestDeadline: Date.now(), -}; - -const reducer: Reducer = (state = defaultState, action: AnyAction) => { - if (isType(action, addEvents)) { - const { payload: events } = action; - const { - events: prevEvents, - inProgressEvents: prevInProgressEvents, - mostRecentEventTime, - } = state; - const updatedEvents = updateEvents(prevEvents, events); - - return { - ...state, - countUnseenEvents: getNumUnseenEvents(updatedEvents), - events: updatedEvents, - inProgressEvents: updateInProgressEvents(prevInProgressEvents, events), - mostRecentEventTime: events.reduce( - mostRecentCreated, - mostRecentEventTime - ), - }; - } - - if (isType(action, updateEventsAsSeen)) { - return { - ...state, - countUnseenEvents: 0, - events: state.events.map((event) => ({ ...event, seen: true })), - }; - } - - if (isType(action, setPollingInterval)) { - return { - ...state, - pollingInterval: action.payload, - }; - } - - if (isType(action, setRequestDeadline)) { - return { - ...state, - requestDeadline: action.payload, - }; - } - - return state; -}; - -export default reducer; diff --git a/packages/manager/src/store/events/event.request.ts b/packages/manager/src/store/events/event.request.ts deleted file mode 100644 index 8d37bb8ee97..00000000000 --- a/packages/manager/src/store/events/event.request.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - getEvents as _getEvents, - Event, - markEventSeen, -} from '@linode/api-v4/lib/account'; -import { DateTime } from 'luxon'; - -import { ISO_DATETIME_NO_TZ_FORMAT } from 'src/constants'; -import { parseAPIDate } from 'src/utilities/date'; -import { generatePollingFilter } from 'src/utilities/requestFilters'; - -import { ThunkActionCreator } from '../types'; -import { addEvents, updateEventsAsSeen } from './event.actions'; -import { epoch, isInProgressEvent, isLongPendingEvent } from './event.helpers'; - -/** - * Will send a filtered request for events which have been created on or after the most recent existing - * event or the epoch if there are no stored events. Exclude events already in memory with a "+neq" filter. - */ -export const getEvents: ThunkActionCreator> = () => ( - dispatch, - getState -) => { - const { - events: _events, - inProgressEvents, - mostRecentEventTime, - } = getState().events; - - // Regardless of date created, we request events that are still in-progress. - const inIds = Object.keys(inProgressEvents).map((thisId) => +thisId); - - // We need to keep polling the event for any database that is still creating. - // The same event will change its status from `notification` to `finished`. - const databaseEventIds = _events - .filter( - (event) => - event.action === 'database_create' && event.status === 'notification' - ) - .map((event) => event.id); - - const includeIds = [...inIds, ...databaseEventIds]; - - // Generate a list of event IDs for the "+neq" filter. We want to request events created - // on or after the most recent created date, minus any events we've already requested. - // This is to catch any events that may be "lost" if the request/query lands at just the - // right moment such that we receive some events with a specific created date, but not all. - const neqIds: number[] = []; - if (_events.length > 0) { - _events.forEach((thisEvent) => { - const thisEventCreated = parseAPIDate(thisEvent.created).valueOf(); - - if ( - thisEventCreated === mostRecentEventTime && - !isInProgressEvent(thisEvent) && - !isLongPendingEvent(thisEvent) - ) { - neqIds.push(thisEvent.id); - } - }); - } - - const filters = generatePollingFilter( - DateTime.fromMillis(mostRecentEventTime, { zone: 'utc' }).toFormat( - ISO_DATETIME_NO_TZ_FORMAT - ), - includeIds, - neqIds - ); - - return ( - _getEvents({ page_size: 25 }, filters) - .then((response) => response.data) - /** - * There is where we set _initial on the events. In the default state of events the - * mostRecentEventTime is set to epoch. On the completion of the first successful events - * update the mostRecentEventTime is updated, meaning it's impossible for subsequent events - * to be incorrectly marked as _initial. This addresses our reappearing toast issue. - */ - .then((events) => - events.map((e) => ({ ...e, _initial: mostRecentEventTime === epoch })) - ) - .then((events) => { - if (events.length > 0) { - dispatch(addEvents(events)); - } - return events; - }) - .catch((e) => []) - ); -}; - -/** - * Send a request to mark all currently stored events as seen, then call updateEventsAsSeen which - * iterates the events and marks them seen. - */ -export const markAllSeen: ThunkActionCreator> = () => ( - dispatch, - getState -) => { - const { - events: { events }, - } = getState(); - /** */ - const latestId = events.reduce( - (result, { id }) => (id > result ? id : result), - 0 - ); - - return markEventSeen(latestId) - .then(() => dispatch(updateEventsAsSeen())) - .catch(() => null); -}; diff --git a/packages/manager/src/store/events/event.selectors.test.ts b/packages/manager/src/store/events/event.selectors.test.ts deleted file mode 100644 index 2dc3680d1c1..00000000000 --- a/packages/manager/src/store/events/event.selectors.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { entityFactory, eventFactory } from 'src/factories/events'; - -import { - isEventRelevantToLinode, - isEventRelevantToLinodeAsSecondaryEntity, - isPrimaryEntity, - isSecondaryEntity, -} from './event.selectors'; - -describe('event selector helpers', () => { - describe('isEventRelevantToLinode', () => { - const event0 = eventFactory.build({ - action: 'linode_create', - entity: entityFactory.build({ id: 0 }), - }); - const event1 = eventFactory.build({ - action: 'linode_create', - entity: entityFactory.build({ id: 1 }), - }); - const event2 = eventFactory.build({ - action: 'linode_create', - entity: entityFactory.build({ id: 100 }), - secondary_entity: entityFactory.build({ id: 1 }), - }); - const event3 = eventFactory.build({ - action: 'linode_clone', - secondary_entity: entityFactory.build({ id: 1 }), - }); - it("returns `true` when the linodeId is the event's entity ID, or if it's the event's secondary_entity ID and the event is relevant", () => { - expect(isEventRelevantToLinode(event0, 0)).toBe(true); - expect(isEventRelevantToLinode(event1, 0)).toBe(false); - expect(isEventRelevantToLinode(event2, 1)).toBe(false); - expect(isEventRelevantToLinode(event3, 1)).toBe(true); - }); - }); - - describe('isPrimaryEntity', () => { - const event = eventFactory.build({ - entity: entityFactory.build({ id: 1 }), - }); - it("returns `true` when the linodeId matches the event's entity ID", () => { - expect(isPrimaryEntity(event, 0)).toBe(false); - expect(isPrimaryEntity(event, 1)).toBe(true); - }); - }); - - describe('isSecondaryEntity', () => { - const event = eventFactory.build({ - secondary_entity: entityFactory.build({ id: 1 }), - }); - it("returns `true` when the linodeId matches the event's secondary_entity ID", () => { - expect(isSecondaryEntity(event, 0)).toBe(false); - expect(isSecondaryEntity(event, 1)).toBe(true); - }); - }); - - describe('isEventRelevantToLinodeAsSecondaryEntity', () => { - const linodeCreateEvent = eventFactory.build({ - action: 'linode_create', - }); - const linodeCloneEvent = eventFactory.build({ - action: 'linode_clone', - }); - it('returns `true` if the event type is relevant to Linodes as secondary entities', () => { - expect(isEventRelevantToLinodeAsSecondaryEntity(linodeCreateEvent)).toBe( - false - ); - expect(isEventRelevantToLinodeAsSecondaryEntity(linodeCloneEvent)).toBe( - true - ); - }); - }); -}); diff --git a/packages/manager/src/store/events/event.selectors.ts b/packages/manager/src/store/events/event.selectors.ts deleted file mode 100644 index a3acebb20b6..00000000000 --- a/packages/manager/src/store/events/event.selectors.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Event, EventAction } from '@linode/api-v4/lib/account'; - -import { State } from './event.reducer'; - -export const eventsForLinode = (state: State, linodeId: number) => { - return state.events.filter((event) => - isEventRelevantToLinode(event, linodeId) - ); -}; - -export const isEventRelevantToLinode = (event: Event, linodeId: number) => - isPrimaryEntity(event, linodeId) || - (isSecondaryEntity(event, linodeId) && - isEventRelevantToLinodeAsSecondaryEntity(event)); - -export const isPrimaryEntity = (event: Event, linodeId: number) => - event?.entity?.type === 'linode' && event?.entity?.id === linodeId; - -export const isSecondaryEntity = (event: Event, linodeId: number) => - event?.secondary_entity?.type === 'linode' && - event?.secondary_entity?.id === linodeId; - -// Some event types include a Linode as a `secondary_entity`. A subset of these -// events should be included in the `eventsForLinode` selector since they are -// relevant to that Linode. -// -// An example: `clone_linode` events include the source Linode as the `entity` -// and the target Linode as the `secondary_entity`. In this case, we want the -// consumer of the `eventsForLinode` selector to have access to these events so -// it can do things like display progress bars. -export const eventActionsForLinodeAsSecondaryEntity: EventAction[] = [ - 'linode_clone', -]; -export const isEventRelevantToLinodeAsSecondaryEntity = (event: Event) => - eventActionsForLinodeAsSecondaryEntity.includes(event?.action); diff --git a/packages/manager/src/store/events/event.types.ts b/packages/manager/src/store/events/event.types.ts deleted file mode 100644 index 20a9d10bee1..00000000000 --- a/packages/manager/src/store/events/event.types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Entity, Event } from '@linode/api-v4/lib/account'; - -export interface ExtendedEvent extends Event { - _deleted?: string; - _initial?: boolean; -} - -export interface EntityEvent extends Omit { - entity: Entity; -} diff --git a/packages/manager/src/store/index.ts b/packages/manager/src/store/index.ts index adcc03c2ccd..6cbf8e201e6 100644 --- a/packages/manager/src/store/index.ts +++ b/packages/manager/src/store/index.ts @@ -1,21 +1,9 @@ -import { QueryClient } from 'react-query'; -import { - Store, - applyMiddleware, - combineReducers, - compose, - createStore, -} from 'redux'; -import thunk from 'redux-thunk'; +import { Store, combineReducers, createStore } from 'redux'; import { State as AuthState } from 'src/store/authentication'; import authentication, { defaultState as authenticationDefaultState, } from 'src/store/authentication/authentication.reducer'; -import events, { - State as EventsState, - defaultState as eventsDefaultState, -} from 'src/store/events/event.reducer'; import globalErrors, { State as GlobalErrorState, defaultState as defaultGlobalErrorState, @@ -37,7 +25,6 @@ import stackScriptDialog, { defaultState as stackScriptDialogDefaultState, } from 'src/store/stackScriptDialog'; -import combineEventsMiddleware from './middleware/combineEventsMiddleware'; import mockFeatureFlags, { MockFeatureFlagState, defaultMockFeatureFlagState, @@ -47,12 +34,9 @@ import pendingUpload, { defaultState as pendingUploadState, } from './pendingUpload'; -const reduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION__; - export interface ApplicationState { authentication: AuthState; createLinode: LinodeCreateState; - events: EventsState; globalErrors: GlobalErrorState; longviewClients: LongviewState; longviewStats: LongviewStatsState; @@ -64,7 +48,6 @@ export interface ApplicationState { export const defaultState: ApplicationState = { authentication: authenticationDefaultState, createLinode: linodeCreateDefaultState, - events: eventsDefaultState, globalErrors: defaultGlobalErrorState, longviewClients: defaultLongviewState, longviewStats: defaultLongviewStatsState, @@ -79,7 +62,6 @@ export const defaultState: ApplicationState = { const reducers = combineReducers({ authentication, createLinode: linodeCreateReducer, - events, globalErrors, longviewClients: longview, longviewStats, @@ -88,14 +70,7 @@ const reducers = combineReducers({ stackScriptDialog, }); -const enhancersFactory = (queryClient: QueryClient) => - compose( - applyMiddleware(thunk, combineEventsMiddleware([], queryClient)), - reduxDevTools ? reduxDevTools() : (f: any) => f - ) as any; - // We need an instance of the query client for some event event handlers -export const storeFactory = (queryClient: QueryClient) => - createStore(reducers, defaultState, enhancersFactory(queryClient)); +export const storeFactory = () => createStore(reducers, defaultState); export type ApplicationStore = Store; diff --git a/packages/manager/src/store/middleware/combineEventsMiddleware.ts b/packages/manager/src/store/middleware/combineEventsMiddleware.ts deleted file mode 100644 index 1d4b6dfa7fd..00000000000 --- a/packages/manager/src/store/middleware/combineEventsMiddleware.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { compose, equals, uniqWith } from 'ramda'; -import { QueryClient } from 'react-query'; -import { Middleware } from 'redux'; -import { isType } from 'typescript-fsa'; - -import { resetEventsPolling } from 'src/eventsPolling'; -import { - isEntityEvent, - isInProgressEvent, - isLongRunningProgressEventAction, -} from 'src/store/events/event.helpers'; -import { EventHandler } from 'src/store/types'; - -import { addEvents } from '../events/event.actions'; -import { ExtendedEvent } from '../events/event.types'; - -const eventsMiddlewareFactory = ( - eventHandlers: EventHandler[], - queryClient: QueryClient -): Middleware => ({ dispatch, getState }) => (next) => (action: any) => { - if (isType(action, addEvents)) { - const { payload } = action; - /** - * We never want to dispatch actions for initial events, so filter them out. - * We also need to only deal with one event per entity at a time, so uniqueEntityEvents - * handles filtering for unique events. Notably linode_create/linode_boot and others. - */ - const eventsToDispatch = compose( - uniqueEntityEvents, - filterInitial - )(payload); - - /** - * The incoming events is an array, usually of one but potentially many, so we have - * to handle each one. - */ - for (const event of eventsToDispatch) { - /** - * We can bail immediately if there is no associated entity since we need an entity - * to update the store. - * - * but we still need to dispatch the action to add the event to the store - */ - if (!isEntityEvent(event)) { - next(action); - return; - } - - /** - * Having an event we care about, we can call the handlers with the event and dispatch. - */ - for (const handler of eventHandlers) { - handler(event, dispatch, getState, queryClient); - } - - /** - * Finally, if any of these events were in-progress we want to reset the events polling - * interval to keep things moving quickly. - */ - if ( - isInProgressEvent(event) && - // Don't poll aggressively on long-running progress events (like migration). - !isLongRunningProgressEventAction(event.action) - ) { - resetEventsPolling(1.5); - } - } - } - - next(action); -}; - -export default eventsMiddlewareFactory; - -/** - * When creating a Linode with an image you get a [ linode_create, linode_boot ] (in order). - * When deleting a Linode (always) you get a [ linode_shutdown, linode_delete ] (in order). - * Here we're taking the first event unique to the entity, so we will never experience - * multiple events being dispatched for a single entity. - * - * We reverse because events come back in reverse order and want the first (boot and delete). - * - * The better user exp during deletion would be to see the shutdown, then the delete, but we - * can't poll for the deleted Linode's status because it's no longer available from the API. - */ -const uniqueEntityEvents = compose( - (e: ExtendedEvent[]) => e.reverse(), - uniqWith((left, right) => - equals(left.entity, right.entity) - ), - (e: ExtendedEvent[]) => e.reverse() -); - -const filterInitial = (events: ExtendedEvent[]) => - events.filter((e) => !e._initial); diff --git a/packages/manager/src/store/selectors/imageEvents.ts b/packages/manager/src/store/selectors/imageEvents.ts deleted file mode 100644 index c92ef4b1708..00000000000 --- a/packages/manager/src/store/selectors/imageEvents.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Event } from '@linode/api-v4/lib/account'; -import { createSelector } from 'reselect'; - -import { ApplicationState } from 'src/store'; -import { - isEventImageUpload, - isEventInProgressDiskImagize, -} from 'src/store/events/event.helpers'; - -/** - * Return a list of all in-progress - * disk_imagize events where the event - * is in progress and has a secondary_entity - * (which will be the actual Image) - */ -export default createSelector( - (state: ApplicationState['events']) => state.events || [], - (events) => - events.filter( - (thisEvent: Event) => - isEventInProgressDiskImagize(thisEvent) || isEventImageUpload(thisEvent) - ) -); diff --git a/packages/manager/src/store/selectors/inProgressEvents.ts b/packages/manager/src/store/selectors/inProgressEvents.ts deleted file mode 100644 index bc3427e0b60..00000000000 --- a/packages/manager/src/store/selectors/inProgressEvents.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createSelector } from 'reselect'; - -import { ApplicationState } from 'src/store'; -import { isInProgressEvent } from 'src/store/events/event.helpers'; - -export default (type: string) => - createSelector( - (state: ApplicationState) => state.events.events, - (events) => - events.filter( - (e) => isInProgressEvent(e) && e.entity && e.entity.type === type - ) - ); diff --git a/packages/manager/src/utilities/promiseToObservable.ts b/packages/manager/src/utilities/promiseToObservable.ts deleted file mode 100644 index 4e70483aacd..00000000000 --- a/packages/manager/src/utilities/promiseToObservable.ts +++ /dev/null @@ -1,33 +0,0 @@ -import 'rxjs/add/operator/switchMap'; -import { fromPromise } from 'rxjs/observable/fromPromise'; - -/** - * This function will transform a given promise into an observable - * but also cancel the previous inner observable in favor of the new one - * - * This lets us prevent overlapping network requests from occurring - * - * learn more about switchMap: - * https://www.learnrxjs.io/operators/transformation/switchmap.html - * - * Example usage: - * - * transformPromiseToCancellableObservable(getLinodes) - * .subscribe( - * () => console.log('onSuccess'), - * () => console.log('onFailure), - * () => console.log('onCompletion') - * ) - * @param promiseFn a function that returns a promise (i.e ) - */ -const transformPromiseToCancellableObservable = ( - promiseFn: () => Promise -) => { - return ( - fromPromise(promiseFn()) - /** cancel previous subscribed request in favor of the new one */ - .switchMap(() => promiseFn()) - ); -}; - -export default transformPromiseToCancellableObservable; diff --git a/packages/manager/src/utilities/testHelpers.tsx b/packages/manager/src/utilities/testHelpers.tsx index 88216873e89..9f153ff41ce 100644 --- a/packages/manager/src/utilities/testHelpers.tsx +++ b/packages/manager/src/utilities/testHelpers.tsx @@ -81,9 +81,7 @@ export const baseStore = (customStore: DeepPartial = {}) => export const wrapWithTheme = (ui: any, options: Options = {}) => { const { customStore, queryClient: passedQueryClient } = options; const queryClient = passedQueryClient ?? queryClientFactory(); - const storeToPass = customStore - ? baseStore(customStore) - : storeFactory(queryClient); + const storeToPass = customStore ? baseStore(customStore) : storeFactory(); // we have to call setupInterceptors so that our API error normalization works as expected // I'm sorry that it makes us pass it the "ApplicationStore" @@ -131,8 +129,7 @@ export const wrapWithStore = (props: { queryClient?: QueryClient; store?: ApplicationStore; }) => { - const queryClient = props.queryClient ?? queryClientFactory(); - const store = props.store ?? storeFactory(queryClient); + const store = props.store ?? storeFactory(); return {props.children}; }; diff --git a/yarn.lock b/yarn.lock index 3a2ec6a8a94..028bff0e695 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12178,13 +12178,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^5.5.6: - version "5.5.12" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" - integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== - dependencies: - symbol-observable "1.0.1" - rxjs@^6.5.2, rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" @@ -12940,11 +12933,6 @@ svg-pathdata@^6.0.3: resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== -symbol-observable@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" - integrity sha512-Kb3PrPYz4HanVF1LVGuAdW6LoVgIwjUYJGzFe7NDrBLCN4lsV/5J0MFurV+ygS4bRVwrCEt2c7MQ1R2a72oJDw== - symbol-observable@^1.0.4: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" From 802e6c345f20bf414c65f1685341fc26fe61793d Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 1 Dec 2023 00:10:13 -0500 Subject: [PATCH 10/44] optimize --- packages/manager/src/queries/events.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index a9dc3d9008a..dd7b2fde969 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -49,11 +49,26 @@ export const useEventsPoller = () => { const latestEventTime = events ? events[0].created : ''; - // @todo run a reducde to optimize - const inProgressEvents = events?.filter(isInProgressEvent); - const eventsThatAlreadyHappenedAtTheFilterTime = events?.filter( - (event) => event.created === latestEventTime - ); + const { + eventsThatAlreadyHappenedAtTheFilterTime, + inProgressEvents, + } = events?.reduce<{ + eventsThatAlreadyHappenedAtTheFilterTime: Event[]; + inProgressEvents: Event[]; + }>( + (acc, event) => { + if (isInProgressEvent(event)) { + acc.inProgressEvents.push(event); + return acc; + } + if (event.created === latestEventTime) { + acc.eventsThatAlreadyHappenedAtTheFilterTime.push(event); + return acc; + } + return acc; + }, + { eventsThatAlreadyHappenedAtTheFilterTime: [], inProgressEvents: [] } + ) ?? { eventsThatAlreadyHappenedAtTheFilterTime: [], inProgressEvents: [] }; const hasFetchedInitialEvents = events !== undefined; From 9a9637a29843f8d182a59e097c3135ee1621fdd6 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 1 Dec 2023 01:15:19 -0500 Subject: [PATCH 11/44] fixes and clean up --- packages/api-v4/src/account/types.ts | 1 - packages/manager/src/__data__/events.ts | 1 - .../manager/src/features/Events/EventRow.tsx | 8 +- .../features/Events/eventMessageGenerator.ts | 5 +- .../manager/src/hooks/useEventHandlers.ts | 32 +- .../src/store/events/event.helpers.test.ts | 445 +++--------------- .../manager/src/store/events/event.helpers.ts | 199 +------- packages/manager/src/store/index.ts | 6 +- .../src/utilities/getEventsActionLink.ts | 8 +- 9 files changed, 81 insertions(+), 624 deletions(-) diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 7826a9f331f..39343b58639 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -404,7 +404,6 @@ export interface Event { time_remaining: null | string; username: string | null; secondary_entity: Entity | null; - _initial?: boolean; message: string | null; } /** diff --git a/packages/manager/src/__data__/events.ts b/packages/manager/src/__data__/events.ts index 0f00ce5052b..9893388014a 100644 --- a/packages/manager/src/__data__/events.ts +++ b/packages/manager/src/__data__/events.ts @@ -574,7 +574,6 @@ export const uniqueEvents: Event[] = [ ]; export const reduxEvent: Event = { - _initial: false, action: 'linode_boot', created: '2018-12-02T20:23:43', duration: 0, diff --git a/packages/manager/src/features/Events/EventRow.tsx b/packages/manager/src/features/Events/EventRow.tsx index 4b1948212ef..87ee1115e8f 100644 --- a/packages/manager/src/features/Events/EventRow.tsx +++ b/packages/manager/src/features/Events/EventRow.tsx @@ -14,18 +14,14 @@ import { getLinkForEvent } from 'src/utilities/getEventsActionLink'; import { StyledGravatar } from './EventRow.styles'; -interface ExtendedEvent extends Event { - _deleted?: string; -} - interface EventRowProps { entityId?: number; - event: ExtendedEvent; + event: Event; } export const EventRow = (props: EventRowProps) => { const { entityId, event } = props; - const link = getLinkForEvent(event.action, event.entity, event._deleted); + const link = getLinkForEvent(event.action, event.entity); const type = pathOr('linode', ['entity', 'type'], event); const timestamp = getEventTimestamp(event); diff --git a/packages/manager/src/features/Events/eventMessageGenerator.ts b/packages/manager/src/features/Events/eventMessageGenerator.ts index 31f5d012476..518984f7687 100644 --- a/packages/manager/src/features/Events/eventMessageGenerator.ts +++ b/packages/manager/src/features/Events/eventMessageGenerator.ts @@ -933,11 +933,10 @@ export function applyLinking(event: Event, message: string) { return ''; } - const entityLinkTarget = getLinkForEvent(event.action, event.entity, false); + const entityLinkTarget = getLinkForEvent(event.action, event.entity); const secondaryEntityLinkTarget = getLinkForEvent( event.action, - event.secondary_entity, - false + event.secondary_entity ); let newMessage = message; diff --git a/packages/manager/src/hooks/useEventHandlers.ts b/packages/manager/src/hooks/useEventHandlers.ts index 4c7660b9b07..631815ce544 100644 --- a/packages/manager/src/hooks/useEventHandlers.ts +++ b/packages/manager/src/hooks/useEventHandlers.ts @@ -24,62 +24,54 @@ export const eventHandlers: { handler: (data: EventHandlerData) => void; }[] = [ { - filter: (event) => event.action.startsWith('database') && !event._initial, + filter: (event) => event.action.startsWith('database'), handler: databaseEventsHandler, }, { filter: (event) => - event.action.startsWith('domain') && - !event._initial && - event.entity !== null, + event.action.startsWith('domain') && event.entity !== null, handler: domainEventsHandler, }, { - filter: (event) => event.action.startsWith('volume') && !event._initial, + filter: (event) => event.action.startsWith('volume'), handler: volumeEventsHandler, }, { filter: (event) => - (event.action.startsWith('image') || event.action === 'disk_imagize') && - !event._initial, + event.action.startsWith('image') || event.action === 'disk_imagize', handler: imageEventsHandler, }, { - filter: (event) => event.action.startsWith('token') && !event._initial, + filter: (event) => event.action.startsWith('token'), handler: tokenEventHandler, }, { - filter: (event) => - event.action.startsWith('user_ssh_key') && !event._initial, + filter: (event) => event.action.startsWith('user_ssh_key'), handler: sshKeyEventHandler, }, { - filter: (event) => event.action.startsWith('firewall') && !event._initial, + filter: (event) => event.action.startsWith('firewall'), handler: firewallEventsHandler, }, { - filter: (event) => - event.action.startsWith('nodebalancer') && !event._initial, + filter: (event) => event.action.startsWith('nodebalancer'), handler: nodebalanacerEventHandler, }, { - filter: (event) => - event.action.startsWith('oauth_client') && !event._initial, + filter: (event) => event.action.startsWith('oauth_client'), handler: oauthClientsEventHandler, }, { filter: (event) => - (event.action.startsWith('linode') || - event.action.startsWith('backups')) && - !event._initial, + event.action.startsWith('linode') || event.action.startsWith('backups'), handler: linodeEventsHandler, }, { - filter: (event) => event.action.startsWith('ticket') && !event._initial, + filter: (event) => event.action.startsWith('ticket'), handler: supportTicketEventHandler, }, { - filter: (event) => event.action.startsWith('disk') && !event._initial, + filter: (event) => event.action.startsWith('disk'), handler: diskEventHandler, }, ]; diff --git a/packages/manager/src/store/events/event.helpers.test.ts b/packages/manager/src/store/events/event.helpers.test.ts index 7918328a4a0..b9d5cb50921 100644 --- a/packages/manager/src/store/events/event.helpers.test.ts +++ b/packages/manager/src/store/events/event.helpers.test.ts @@ -1,417 +1,86 @@ -import { Event } from '@linode/api-v4/lib/account'; -import { DateTime } from 'luxon'; - import { entityFactory, eventFactory } from 'src/factories/events'; import { - addToEvents, - findInEvents, - getNumUnseenEvents, - isCompletedEvent, isEventRelevantToLinode, isEventRelevantToLinodeAsSecondaryEntity, isInProgressEvent, isPrimaryEntity, isSecondaryEntity, - mostRecentCreated, - setDeletedEvents, - updateInProgressEvents, } from './event.helpers'; -describe('event.helpers', () => { - describe('findInEvents', () => { - const entity = { - id: 1, - label: 'something', - type: 'whatever', - url: 'whoecares', - }; - - const event: Pick = { entity }; - - it('should return index when entity is found', () => { - const result = findInEvents([event], entity); - expect(result).toBe(0); - }); - - it('should return index when entity is found', () => { - const result = findInEvents([], entity); - expect(result).toBe(-1); - }); +describe('isInProgressEvent', () => { + it('should return true', () => { + const event = eventFactory.build({ percent_complete: 60 }); + const result = isInProgressEvent(event); + expect(result).toBeTruthy(); }); - describe('getNumUnseenEvents', () => { - it('should return number of unseen events', () => { - const events = [ - { seen: true }, - { seen: false }, - { seen: false }, - { seen: true }, - { seen: false }, - { seen: true }, - ]; - const result = getNumUnseenEvents(events); - expect(result).toBe(3); - }); + it('should return false', () => { + const event = eventFactory.build({ percent_complete: 100 }); + const result = isInProgressEvent(event); + expect(result).toBeFalsy(); }); +}); - describe('isCompletedEvent', () => { - it('should return true', () => { - const event = { percent_complete: 100 }; - const result = isCompletedEvent(event); - expect(result).toBeTruthy(); - }); - - it('should return false', () => { - const event = { percent_complete: 60 }; - const result = isCompletedEvent(event); - expect(result).toBeFalsy(); - }); +describe('isEventRelevantToLinode', () => { + const event0 = eventFactory.build({ + action: 'linode_create', + entity: entityFactory.build({ id: 0 }), }); - - describe('isInProgressEvent', () => { - it('should return true', () => { - const event = eventFactory.build({ percent_complete: 60 }); - const result = isInProgressEvent(event); - expect(result).toBeTruthy(); - }); - - it('should return false', () => { - const event = eventFactory.build({ percent_complete: 100 }); - const result = isInProgressEvent(event); - expect(result).toBeFalsy(); - }); + const event1 = eventFactory.build({ + action: 'linode_create', + entity: entityFactory.build({ id: 1 }), }); - - describe('mostRecentCreated', () => { - it('should return the most recent event time', () => { - expect( - mostRecentCreated(new Date(`1970-01-01T00:00:00`).getTime(), { - created: `2018-12-03T22:37:20`, - }) - ).toBe( - DateTime.fromISO(`2018-12-03T22:37:20`, { zone: 'UTC' }).valueOf() - ); - - const recentTime = DateTime.fromISO(`2018-12-03T23:37:20`, { - zone: 'UTC', - }).valueOf(); - expect( - mostRecentCreated(recentTime, { created: `2018-12-03T22:37:20` }) - ).toBe(recentTime); - }); + const event2 = eventFactory.build({ + action: 'linode_create', + entity: entityFactory.build({ id: 100 }), + secondary_entity: entityFactory.build({ id: 1 }), }); - - describe('setDeletedEvents', () => { - it('should add a _deleted prop if entity refers to a deleted entity', () => { - const events: Event[] = [ - { - action: 'linode_delete', - created: '2018-12-02T23:15:45', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17957944, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:55:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17957108, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - ]; - - const expected: Event[] = [ - { - action: 'linode_delete', - created: '2018-12-02T23:15:45', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17957944, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:55:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17957108, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - ]; - - const result = setDeletedEvents(events); - expect(result).toEqual(expected); - }); + const event3 = eventFactory.build({ + action: 'linode_clone', + secondary_entity: entityFactory.build({ id: 1 }), }); - - describe('addToEvents', () => { - it('should append the event to the list', () => { - const prevEvents: Event[] = [ - { - action: 'linode_delete', - created: '2018-12-02T23:15:45', - duration: 0, - entity: null, - id: 17957944, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T23:08:38', - duration: 0, - entity: null, - id: 17957718, - message: null, - percent_complete: 60, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'started', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:55:43', - duration: 0, - entity: null, - id: 17957108, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - ]; - const events: Event[] = [ - { - action: 'linode_shutdown', - created: '2018-12-02T23:08:38', - duration: 0, - entity: null, - id: 17957718, - message: null, - percent_complete: 70, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'started', - time_remaining: null, - username: 'test', - }, - ]; - const result = addToEvents(prevEvents, events); - - expect(result).toEqual([ - { - action: 'linode_delete', - created: '2018-12-02T23:15:45', - duration: 0, - entity: null, - id: 17957944, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T23:08:38', - duration: 0, - entity: null, - id: 17957718, - message: null, - percent_complete: 70, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'started', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:55:43', - duration: 0, - entity: null, - id: 17957108, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - ]); - }); + it("returns `true` when the linodeId is the event's entity ID, or if it's the event's secondary_entity ID and the event is relevant", () => { + expect(isEventRelevantToLinode(event0, 0)).toBe(true); + expect(isEventRelevantToLinode(event1, 0)).toBe(false); + expect(isEventRelevantToLinode(event2, 1)).toBe(false); + expect(isEventRelevantToLinode(event3, 1)).toBe(true); }); +}); - describe('updateInProgressEvents', () => { - it('should do nothing if there are no events', () => { - const inProgressEvents = {}; - const events: Event[] = []; - const result = updateInProgressEvents(inProgressEvents, events); - expect(result).toEqual({}); - }); - - it('should do nothing if there are no in-progress events', () => { - const inProgressEvents = { '999': 23 }; - const events = eventFactory.buildList(3, { percent_complete: 100 }); - const result = updateInProgressEvents(inProgressEvents, events); - expect(result).toEqual({ '999': 23 }); - }); - - it('should add in-progress events to the Map', () => { - const inProgressEvents = {}; - const events = eventFactory.buildList(3, { percent_complete: 100 }); - // Mark one event as in progress - events[1].percent_complete = 60; - const result = updateInProgressEvents(inProgressEvents, events); - - expect(result).toEqual({ [events[1].id]: 60 }); - }); +describe('isPrimaryEntity', () => { + const event = eventFactory.build({ + entity: entityFactory.build({ id: 1 }), + }); + it("returns `true` when the linodeId matches the event's entity ID", () => { + expect(isPrimaryEntity(event, 0)).toBe(false); + expect(isPrimaryEntity(event, 1)).toBe(true); }); }); -describe('event selector helpers', () => { - describe('isEventRelevantToLinode', () => { - const event0 = eventFactory.build({ - action: 'linode_create', - entity: entityFactory.build({ id: 0 }), - }); - const event1 = eventFactory.build({ - action: 'linode_create', - entity: entityFactory.build({ id: 1 }), - }); - const event2 = eventFactory.build({ - action: 'linode_create', - entity: entityFactory.build({ id: 100 }), - secondary_entity: entityFactory.build({ id: 1 }), - }); - const event3 = eventFactory.build({ - action: 'linode_clone', - secondary_entity: entityFactory.build({ id: 1 }), - }); - it("returns `true` when the linodeId is the event's entity ID, or if it's the event's secondary_entity ID and the event is relevant", () => { - expect(isEventRelevantToLinode(event0, 0)).toBe(true); - expect(isEventRelevantToLinode(event1, 0)).toBe(false); - expect(isEventRelevantToLinode(event2, 1)).toBe(false); - expect(isEventRelevantToLinode(event3, 1)).toBe(true); - }); +describe('isSecondaryEntity', () => { + const event = eventFactory.build({ + secondary_entity: entityFactory.build({ id: 1 }), }); - - describe('isPrimaryEntity', () => { - const event = eventFactory.build({ - entity: entityFactory.build({ id: 1 }), - }); - it("returns `true` when the linodeId matches the event's entity ID", () => { - expect(isPrimaryEntity(event, 0)).toBe(false); - expect(isPrimaryEntity(event, 1)).toBe(true); - }); + it("returns `true` when the linodeId matches the event's secondary_entity ID", () => { + expect(isSecondaryEntity(event, 0)).toBe(false); + expect(isSecondaryEntity(event, 1)).toBe(true); }); +}); - describe('isSecondaryEntity', () => { - const event = eventFactory.build({ - secondary_entity: entityFactory.build({ id: 1 }), - }); - it("returns `true` when the linodeId matches the event's secondary_entity ID", () => { - expect(isSecondaryEntity(event, 0)).toBe(false); - expect(isSecondaryEntity(event, 1)).toBe(true); - }); +describe('isEventRelevantToLinodeAsSecondaryEntity', () => { + const linodeCreateEvent = eventFactory.build({ + action: 'linode_create', }); - - describe('isEventRelevantToLinodeAsSecondaryEntity', () => { - const linodeCreateEvent = eventFactory.build({ - action: 'linode_create', - }); - const linodeCloneEvent = eventFactory.build({ - action: 'linode_clone', - }); - it('returns `true` if the event type is relevant to Linodes as secondary entities', () => { - expect(isEventRelevantToLinodeAsSecondaryEntity(linodeCreateEvent)).toBe( - false - ); - expect(isEventRelevantToLinodeAsSecondaryEntity(linodeCloneEvent)).toBe( - true - ); - }); + const linodeCloneEvent = eventFactory.build({ + action: 'linode_clone', + }); + it('returns `true` if the event type is relevant to Linodes as secondary entities', () => { + expect(isEventRelevantToLinodeAsSecondaryEntity(linodeCreateEvent)).toBe( + false + ); + expect(isEventRelevantToLinodeAsSecondaryEntity(linodeCloneEvent)).toBe( + true + ); }); }); diff --git a/packages/manager/src/store/events/event.helpers.ts b/packages/manager/src/store/events/event.helpers.ts index fdd23c97495..8bc8db0a6ef 100644 --- a/packages/manager/src/store/events/event.helpers.ts +++ b/packages/manager/src/store/events/event.helpers.ts @@ -1,151 +1,4 @@ -import { - Entity, - Event, - EventAction, - EventStatus, -} from '@linode/api-v4/lib/account'; -import { compose, equals, findIndex, omit, take, update } from 'ramda'; - -import { parseAPIDate } from 'src/utilities/date'; -import { updateRight } from 'src/utilities/updateRight'; - -/** We use the epoch on our initial request to get all of the users events. */ -export const epoch = new Date(`1970-01-01T00:00:00.000`).getTime(); - -/** - * isRelevantDeletionEvent - * - * Returns `true` if the event: - * a) has _delete in its action (so it's a deletion event) - * b) is not a special case (see below). - * c) the event indicates a non-failed deletion action - * - * If these conditions are met, the entity that the event is attached to - * is assumed to no longer exist in the database. - * - * @param action - */ -export const isRelevantDeletionEvent = ( - action: EventAction, - status: EventStatus -) => { - /** - * These events point to a Linode, not a disk/config/etc., - * but the Linode most likely still exists so we shouldn't mark - * all events related to that Linode as _deleted. - */ - const ignoredDeletionEvents = ['linode_config_delete', 'disk_delete']; - if (ignoredDeletionEvents.includes(action)) { - return false; - } - return action.includes(`_delete`) && status !== 'failed'; -}; - -/** - * Safely find an entity in a list of entities returning the index. - * Will return -1 if the index is not found. - * - * entities {Entity[]} - * entity {null | Entity} - */ -export const findInEvents = ( - events: Pick[], - entity: Partial | null = {} -) => findIndex((e) => equals(e.entity, entity), events); - -export const setDeletedEvents = (events: Event[]) => { - /** Create a list of deletion events. */ - const deletions = events.reduce((result: Event[], event) => { - const { action, entity, status } = event; - if (!entity) { - return result; - } - - if (!isRelevantDeletionEvent(action, status)) { - /** - * This is either a deletion event that hasn't finished - * (so the entity still exists) or it's something like - * disk_delete, where the entity itself (the Linode) - * has not been deleted. - */ - return result; - } - /** - * If we get all the way down here, we have an event - * which indicates that an entity has been deleted; - * add it to the list of deletion events. - */ - return [event, ...result]; - }, []); - - /** If there are no deletions to process, just return the events. */ - if (deletions.length === 0) { - return events; - } - - /** Map events to either deleted or not. */ - return events.map((e) => { - const indexOfFoundEvent = findInEvents(deletions, e.entity); - - return indexOfFoundEvent > -1 - ? { ...e, _deleted: deletions[indexOfFoundEvent].created } - : e; - }); -}; - -export const updateEvents = compose( - /** Finally we return the updated state (right) */ - ([prevEvents, events]) => events, - - /** Nested compose to get around Ramda's shotty typing. */ - compose( - /** Take only the last 100 events. */ - updateRight((prevEvents, events) => take(100, events)), - - /** Marked events "_deleted". */ - updateRight((prevEvents, events) => - setDeletedEvents(events) - ), - - /** Add events to the list. */ - updateRight((prevEvents, events) => - addToEvents(prevEvents, events) - ) - ), - - /** Convert the arguments to a tuple so we can use updateRight. */ - (prevEvents: Event[], events: Event[]) => [prevEvents, events] -); - -/** - * Compare the latestTime with the given Linode's created time and return the most recent. - * - */ -export const mostRecentCreated = ( - latestTime: number, - current: Pick -) => { - const time: number = parseAPIDate(current.created).valueOf(); // Unix time (milliseconds) - return latestTime > time ? latestTime : time; -}; - -/** - * Compile an updated list of events by either updating an event in place or prepending an event - * to prevEvents. - * - * I know this could be much more generic, but I cant get the typing right. - */ -export const addToEvents = (prevArr: Event[], arr: Event[]) => - arr.reduceRight((updatedArray, el) => { - /** - * We need to update in place to maintain the correct timeline of events. Update in-place - * by finding the index then updating at that index. - */ - const indexOfFoundEvent = findIndex(({ id }) => id === el.id, updatedArray); - return indexOfFoundEvent > -1 - ? update(indexOfFoundEvent, el, updatedArray) - : [el, ...updatedArray]; - }, prevArr); +import { Event, EventAction } from '@linode/api-v4/lib/account'; export const isLongPendingEvent = (event: Event): boolean => { const { action, status } = event; @@ -161,20 +14,6 @@ export const isInProgressEvent = (event: Event) => { } }; -export const isLongRunningProgressEventAction = (eventAction: EventAction) => { - const longRunningProgressEventActions: EventAction[] = [ - 'linode_resize', - 'linode_migrate', - 'linode_migrate_datacenter', - ]; - return longRunningProgressEventActions.includes(eventAction); -}; - -export const isCompletedEvent = ({ - percent_complete, -}: Pick) => - percent_complete !== null && percent_complete === 100; - export const isEventInProgressDiskImagize = (event: Event): boolean => { return ( event.action === 'disk_imagize' && @@ -187,40 +26,6 @@ export const isEventImageUpload = (event: Event): boolean => { return event.action === 'image_upload'; }; -/** - * Iterate through new events. - * If an event is "in-progress" it's added to the inProgressEvents map. - * If an event is "completed" it is removed from the inProgressEvents map. - * Otherwise the inProgressEvents is unchanged. - * - * @returns { [key: number]: number } inProgressEvents: key value pair, where the - * key will be the ID of the event and the value will be the percent_complete - * - */ -export const updateInProgressEvents = ( - inProgressEvents: Record, - event: Event[] -) => { - return event.reduce((result, e) => { - const key = String(e.id); - - if (isCompletedEvent(e)) { - return omit([key], result); - } - - return isInProgressEvent(e) - ? { ...result, [key]: e.percent_complete } - : result; - }, inProgressEvents); -}; - -export const getNumUnseenEvents = (events: Pick[]) => - events.reduce((result, event) => (event.seen ? result : result + 1), 0); - -export const eventsForLinode = (events: Event[], linodeId: number) => { - return events.filter((event) => isEventRelevantToLinode(event, linodeId)); -}; - export const isEventRelevantToLinode = (event: Event, linodeId: number) => isPrimaryEntity(event, linodeId) || (isSecondaryEntity(event, linodeId) && @@ -245,4 +50,4 @@ export const eventActionsForLinodeAsSecondaryEntity: EventAction[] = [ 'linode_clone', ]; export const isEventRelevantToLinodeAsSecondaryEntity = (event: Event) => - eventActionsForLinodeAsSecondaryEntity.includes(event?.action) \ No newline at end of file + eventActionsForLinodeAsSecondaryEntity.includes(event?.action); diff --git a/packages/manager/src/store/index.ts b/packages/manager/src/store/index.ts index 6cbf8e201e6..1daf1522c69 100644 --- a/packages/manager/src/store/index.ts +++ b/packages/manager/src/store/index.ts @@ -1,4 +1,4 @@ -import { Store, combineReducers, createStore } from 'redux'; +import { Store, applyMiddleware, combineReducers, createStore } from 'redux'; import { State as AuthState } from 'src/store/authentication'; import authentication, { @@ -33,6 +33,7 @@ import pendingUpload, { State as PendingUploadState, defaultState as pendingUploadState, } from './pendingUpload'; +import thunk from 'redux-thunk'; export interface ApplicationState { authentication: AuthState; @@ -71,6 +72,7 @@ const reducers = combineReducers({ }); // We need an instance of the query client for some event event handlers -export const storeFactory = () => createStore(reducers, defaultState); +export const storeFactory = () => + createStore(reducers, defaultState, applyMiddleware(thunk)); export type ApplicationStore = Store; diff --git a/packages/manager/src/utilities/getEventsActionLink.ts b/packages/manager/src/utilities/getEventsActionLink.ts index b73c0801281..20d24bf41d5 100644 --- a/packages/manager/src/utilities/getEventsActionLink.ts +++ b/packages/manager/src/utilities/getEventsActionLink.ts @@ -6,11 +6,7 @@ export const getEngineFromDatabaseEntityURL = (url: string) => { return url.match(/databases\/(\w*)\/instances/i)?.[1]; }; -export const getLinkForEvent = ( - action: EventAction, - entity: Entity | null, - deleted: boolean | string | undefined -) => { +export const getLinkForEvent = (action: EventAction, entity: Entity | null) => { const type = entity?.type; const id = entity?.id; const label = entity?.label; @@ -40,7 +36,7 @@ export const getLinkForEvent = ( * If we have a deletion event or an event that is marked as referring to a deleted entity * we don't want a clickable action. */ - if (action.includes('_delete') || deleted) { + if (action.includes('_delete')) { return; } From 5dec126da2d39a20cc032c3d7f5f2747a3e5b995 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 1 Dec 2023 11:35:25 -0500 Subject: [PATCH 12/44] fix cache updater bug --- packages/manager/src/queries/events.ts | 66 +++++++++++++------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index dd7b2fde969..46c458bf381 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -53,16 +53,16 @@ export const useEventsPoller = () => { eventsThatAlreadyHappenedAtTheFilterTime, inProgressEvents, } = events?.reduce<{ - eventsThatAlreadyHappenedAtTheFilterTime: Event[]; - inProgressEvents: Event[]; + eventsThatAlreadyHappenedAtTheFilterTime: number[]; + inProgressEvents: number[]; }>( (acc, event) => { if (isInProgressEvent(event)) { - acc.inProgressEvents.push(event); + acc.inProgressEvents.push(event.id); return acc; } if (event.created === latestEventTime) { - acc.eventsThatAlreadyHappenedAtTheFilterTime.push(event); + acc.eventsThatAlreadyHappenedAtTheFilterTime.push(event.id); return acc; } return acc; @@ -74,8 +74,8 @@ export const useEventsPoller = () => { const filter = generatePollingFilter( latestEventTime, - inProgressEvents?.map((event) => event.id), - eventsThatAlreadyHappenedAtTheFilterTime?.map((e) => e.id) + inProgressEvents, + eventsThatAlreadyHappenedAtTheFilterTime ); useQuery({ @@ -83,45 +83,47 @@ export const useEventsPoller = () => { onSuccess(events) { incrementPollingInterval(); - const { existingEvents, newEvents } = events.reduce<{ - existingEvents: Event[]; - newEvents: Event[]; - }>( - (acc, event) => { - if (inProgressEvents?.some((e) => e.id === event.id)) { - acc.existingEvents.push(event); - } else { - acc.newEvents.push(event); - } - return acc; - }, - { existingEvents: [], newEvents: [] } - ); - queryClient.setQueryData>>( ['events', 'infinite', {}], (prev) => { - const newPages = prev?.pages.map((page, index) => { - if (index === 0 && newEvents.length > 0) { - page.data = [...newEvents, ...page.data]; - } + if (!prev) { + return { + pages: [], + pageParams: [], + }; + } - for (const event of existingEvents) { + const updatedEventIndexes: number[] = []; + + for (const page of prev.pages) { + for (let i = 0; i < events.length; i++) { const indexOfEvent = page.data.findIndex( - (e) => e.id === event.id + (e) => e.id === events[i].id ); if (indexOfEvent !== -1) { - page.data[index] = event; + page.data[indexOfEvent] = events[i]; + updatedEventIndexes.push(i); } } + } - return page; - }); + 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.pages[0].data = [...newEvents, ...prev.pages[0].data]; + } return { - pageParams: prev?.pageParams ?? [], - pages: newPages ?? [], + pageParams: prev.pageParams, + pages: prev.pages, }; } ); From 0a15104b20b0e6f650b41dd2c038ef1672db3d17 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 1 Dec 2023 13:45:20 -0500 Subject: [PATCH 13/44] make the query updater update all queries --- packages/manager/src/queries/events.ts | 138 +++++++++++++++++-------- 1 file changed, 93 insertions(+), 45 deletions(-) diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 46c458bf381..3da352d7242 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -1,6 +1,8 @@ import { getEvents, markEventSeen } from '@linode/api-v4'; import { InfiniteData, + QueryClient, + QueryKey, useInfiniteQuery, useMutation, useQuery, @@ -15,13 +17,15 @@ import { generatePollingFilter } from 'src/utilities/requestFilters'; import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; -export const useEventsInfiniteQuery = (filter: Filter = {}) => { +export const useEventsInfiniteQuery = (filter?: Filter) => { const query = useInfiniteQuery, APIError[]>( ['events', 'infinite', filter], ({ pageParam }) => getEvents({ page: pageParam }, filter), { + cacheTime: Infinity, getNextPageParam: ({ page, pages }) => page < pages ? page + 1 : undefined, + staleTime: Infinity, } ); @@ -83,50 +87,7 @@ export const useEventsPoller = () => { onSuccess(events) { incrementPollingInterval(); - queryClient.setQueryData>>( - ['events', 'infinite', {}], - (prev) => { - if (!prev) { - return { - pages: [], - pageParams: [], - }; - } - - const updatedEventIndexes: number[] = []; - - for (const page of prev.pages) { - for (let i = 0; i < events.length; i++) { - const indexOfEvent = page.data.findIndex( - (e) => e.id === events[i].id - ); - - if (indexOfEvent !== -1) { - page.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.pages[0].data = [...newEvents, ...prev.pages[0].data]; - } - - return { - pageParams: prev.pageParams, - pages: prev.pages, - }; - } - ); + updateEventsQueries(events, queryClient); for (const event of events) { for (const eventHandler of eventHandlers) { @@ -202,3 +163,90 @@ export const useMarkEventsAsSeen = () => { } ); }; + +export const updateEventsQueries = ( + events: Event[], + queryClient: QueryClient +) => { + queryClient + .getQueryCache() + .findAll(['events', 'infinite']) + .forEach(({ queryKey }) => { + const apiFilter = queryKey[queryKey.length - 1] as Filter | undefined; + + if (apiFilter === undefined) { + updateEventsQuery(events, queryKey, queryClient); + return; + } + + const filteredEvents = events.filter((event) => { + // @ts-expect-error todo fix filter type + const notEqualItems = apiFilter.action?.['+neq']; + if (notEqualItems && notEqualItems.includes(event.action)) { + return false; + } + if ( + apiFilter?.['entity.id'] && + apiFilter?.['entity.type'] && + apiFilter['entity.id'] !== event.entity?.id && + apiFilter['entity.type'] !== event.entity?.type + ) { + return false; + } + return true; + }); + + updateEventsQuery(filteredEvents, queryKey, queryClient); + }); +}; + +export const updateEventsQuery = ( + events: Event[], + queryKey: QueryKey, + queryClient: QueryClient +) => { + queryClient.setQueryData>>( + queryKey, + (prev) => { + if (!prev) { + return { + pageParams: [], + pages: [], + }; + } + + const updatedEventIndexes: number[] = []; + + for (const page of prev.pages) { + for (let i = 0; i < events.length; i++) { + const indexOfEvent = page.data.findIndex( + (e) => e.id === events[i].id + ); + + if (indexOfEvent !== -1) { + page.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.pages[0].data = [...newEvents, ...prev.pages[0].data]; + } + + return { + pageParams: prev.pageParams, + pages: prev.pages, + }; + } + ); +}; From 9b4ed1bcc6430d6f75b7690e8c7fe26564b34678 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 1 Dec 2023 14:28:21 -0500 Subject: [PATCH 14/44] poc: use cursor based pagination --- packages/manager/src/queries/events.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 3da352d7242..97379cce6e4 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -20,17 +20,22 @@ import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; export const useEventsInfiniteQuery = (filter?: Filter) => { const query = useInfiniteQuery, APIError[]>( ['events', 'infinite', filter], - ({ pageParam }) => getEvents({ page: pageParam }, filter), + ({ pageParam }) => + getEvents({}, { ...filter, created: { '+lt': pageParam } }), { cacheTime: Infinity, - getNextPageParam: ({ page, pages }) => - page < pages ? page + 1 : undefined, + getNextPageParam: ({ data, results }) => { + if (results === data.length) { + return undefined; + } + return data[data.length - 1].created; + }, staleTime: Infinity, } ); const events = query.data?.pages.reduce( - (events, page) => [...page.data, ...events], + (events, page) => [...events, ...page.data], [] ); @@ -131,7 +136,7 @@ export const useMarkEventsAsSeen = () => { { onSuccess: (_, eventId) => { queryClient.setQueryData>>( - ['events', 'infinite', {}], + ['events', 'infinite', undefined], (prev) => { if (!prev) { return { From eaeba01c79550cb2cc8ce47525701a3c68ad3b2b Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 14 Dec 2023 09:55:11 -0500 Subject: [PATCH 15/44] remove old code --- packages/manager/src/hooks/useInitialRequests.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/manager/src/hooks/useInitialRequests.ts b/packages/manager/src/hooks/useInitialRequests.ts index 56a33529db7..595e03e3f61 100644 --- a/packages/manager/src/hooks/useInitialRequests.ts +++ b/packages/manager/src/hooks/useInitialRequests.ts @@ -84,9 +84,6 @@ export const useInitialRequests = () => { }), ]; - // Start events polling - // startEventsInterval(store, queryClient); - try { await Promise.all(dataFetchingPromises); } finally { From 10fb540dbeb1d071150c9d1433453c09482a796a Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 14 Dec 2023 10:48:38 -0500 Subject: [PATCH 16/44] use `id` for cusour based pagination and clean up --- .../manager/src/hooks/useEventHandlers.ts | 21 +++++++++++++++++++ packages/manager/src/queries/events.ts | 21 +++++++++---------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/manager/src/hooks/useEventHandlers.ts b/packages/manager/src/hooks/useEventHandlers.ts index 631815ce544..73693eceed1 100644 --- a/packages/manager/src/hooks/useEventHandlers.ts +++ b/packages/manager/src/hooks/useEventHandlers.ts @@ -1,3 +1,5 @@ +import { useQueryClient } from 'react-query'; + import { oauthClientsEventHandler } from 'src/queries/accountOAuth'; import { databaseEventsHandler } from 'src/queries/databases'; import { domainEventsHandler } from 'src/queries/domains'; @@ -75,3 +77,22 @@ export const eventHandlers: { handler: diskEventHandler, }, ]; + +export const useEventHandlers = () => { + const queryClient = useQueryClient(); + + /** + * Given an event, this function finds the corresponding + * event handler and invokes it. + */ + const handleEvent = (event: Event) => { + for (const eventHandler of eventHandlers) { + if (eventHandler.filter(event)) { + eventHandler.handler({ event, queryClient }); + return; + } + } + }; + + return { handleEvent }; +}; diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 97379cce6e4..4ba31a00c57 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -10,7 +10,7 @@ import { } from 'react-query'; import { DISABLE_EVENT_THROTTLE, INTERVAL } from 'src/constants'; -import { eventHandlers } from 'src/hooks/useEventHandlers'; +import { useEventHandlers } from 'src/hooks/useEventHandlers'; import { useToastNotifications } from 'src/hooks/useToastNotifications'; import { isInProgressEvent } from 'src/store/events/event.helpers'; import { generatePollingFilter } from 'src/utilities/requestFilters'; @@ -20,15 +20,14 @@ import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; export const useEventsInfiniteQuery = (filter?: Filter) => { const query = useInfiniteQuery, APIError[]>( ['events', 'infinite', filter], - ({ pageParam }) => - getEvents({}, { ...filter, created: { '+lt': pageParam } }), + ({ pageParam }) => getEvents({}, { ...filter, id: { '+lt': pageParam } }), { cacheTime: Infinity, getNextPageParam: ({ data, results }) => { if (results === data.length) { return undefined; } - return data[data.length - 1].created; + return data[data.length - 1].id; }, staleTime: Infinity, } @@ -52,6 +51,8 @@ export const useEventsPoller = () => { const { incrementPollingInterval, pollingInterval } = usePollingInterval(); const { handleGlobalToast } = useToastNotifications(); + const { handleEvent } = useEventHandlers(); + const queryClient = useQueryClient(); const { events } = useEventsInfiniteQuery(); @@ -92,15 +93,13 @@ export const useEventsPoller = () => { onSuccess(events) { incrementPollingInterval(); - updateEventsQueries(events, queryClient); + if (events.length > 0) { + updateEventsQueries(events, queryClient); - for (const event of events) { - for (const eventHandler of eventHandlers) { - if (eventHandler.filter(event)) { - eventHandler.handler({ event, queryClient }); - } + for (const event of events) { + handleGlobalToast(event); + handleEvent(event); } - handleGlobalToast(event); } }, queryFn: () => getEvents({}, filter).then((data) => data.data), From fab6a546ee617c67ad91ad23a1f88304a85f4c8c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 14 Dec 2023 11:50:38 -0500 Subject: [PATCH 17/44] move things around to minimize re-renders --- packages/manager/src/App.tsx | 20 +++++++++----------- packages/manager/src/GoTo.tsx | 20 +++++++++++--------- packages/manager/src/queries/events.ts | 2 ++ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index 3419f9e4c34..886018af986 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -14,10 +14,8 @@ import { GoTo } from './GoTo'; import { MainContent } from './MainContent'; import { SplashScreen } from './components/SplashScreen'; import { useAdobeAnalytics } from './hooks/useAdobeAnalytics'; -import { useGlobalKeyboardListener } from './hooks/useGlobalKeyboardListener'; import { useInitialRequests } from './hooks/useInitialRequests'; import { useNewRelic } from './hooks/useNewRelic'; -import { useToastNotifications } from './hooks/useToastNotifications'; import { useEventsPoller } from './queries/events'; import { useSetupFeatureFlags } from './useSetupFeatureFlags'; @@ -31,14 +29,6 @@ const BaseApp = withDocumentTitleProvider( const { areFeatureFlagsLoading } = useSetupFeatureFlags(); - const { goToOpen, setGoToOpen } = useGlobalKeyboardListener(); - - useEventsPoller(); - useToastNotifications(); - - useAdobeAnalytics(); - useNewRelic(); - if (isLoading || areFeatureFlagsLoading) { return ; } @@ -56,11 +46,19 @@ const BaseApp = withDocumentTitleProvider( Opens an external site in a new window
- setGoToOpen(false)} open={goToOpen} /> + + ); }) ) ); + +const GlobalListeners = () => { + useEventsPoller(); + useAdobeAnalytics(); + useNewRelic(); + return null; +}; diff --git a/packages/manager/src/GoTo.tsx b/packages/manager/src/GoTo.tsx index 32af40cb471..610878981f5 100644 --- a/packages/manager/src/GoTo.tsx +++ b/packages/manager/src/GoTo.tsx @@ -8,6 +8,7 @@ import EnhancedSelect, { Item } from 'src/components/EnhancedSelect/Select'; import { useAccountManagement } from './hooks/useAccountManagement'; import { useFlags } from './hooks/useFlags'; +import { useGlobalKeyboardListener } from './hooks/useGlobalKeyboardListener'; const useStyles = makeStyles((theme: Theme) => ({ input: { @@ -53,20 +54,21 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -interface Props { - onClose: () => void; - open: boolean; -} - -export const GoTo = React.memo((props: Props) => { +export const GoTo = React.memo(() => { const classes = useStyles(); const routerHistory = useHistory(); const { _hasAccountAccess, _isManagedAccount } = useAccountManagement(); const flags = useFlags(); + const { goToOpen, setGoToOpen } = useGlobalKeyboardListener(); + + const onClose = () => { + setGoToOpen(false); + }; + const onSelect = (item: Item) => { routerHistory.push(item.value); - props.onClose(); + onClose(); }; const links = React.useMemo( @@ -169,8 +171,8 @@ export const GoTo = React.memo((props: Props) => { return ( {/* I was about to put a "@todo" item for mobile display, but realized diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 4ba31a00c57..89274e87637 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -106,6 +106,8 @@ export const useEventsPoller = () => { queryKey: ['events', 'poller'], refetchInterval: pollingInterval, }); + + return null; }; export const usePollingInterval = () => { From da30f273a2862cf2a1a1a44c28e67c7b49857cb2 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 14 Dec 2023 13:05:06 -0500 Subject: [PATCH 18/44] handle accounts with no events and fix queryKey bug --- packages/manager/src/queries/events.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 89274e87637..fde4fcaff05 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -1,4 +1,6 @@ import { getEvents, markEventSeen } from '@linode/api-v4'; +import { DateTime } from 'luxon'; +import { useState } from 'react'; import { InfiniteData, QueryClient, @@ -9,7 +11,11 @@ import { useQueryClient, } from 'react-query'; -import { DISABLE_EVENT_THROTTLE, INTERVAL } from 'src/constants'; +import { + DISABLE_EVENT_THROTTLE, + INTERVAL, + ISO_DATETIME_NO_TZ_FORMAT, +} from 'src/constants'; import { useEventHandlers } from 'src/hooks/useEventHandlers'; import { useToastNotifications } from 'src/hooks/useToastNotifications'; import { isInProgressEvent } from 'src/store/events/event.helpers'; @@ -57,7 +63,16 @@ export const useEventsPoller = () => { const { events } = useEventsInfiniteQuery(); - const latestEventTime = events ? events[0].created : ''; + const [mountTimestamp] = useState(() => + DateTime.fromMillis(Date.now(), { zone: 'utc' }).toFormat( + ISO_DATETIME_NO_TZ_FORMAT + ) + ); + + // 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. + const latestEventTime = + events && events.length > 0 ? events[0].created : mountTimestamp; const { eventsThatAlreadyHappenedAtTheFilterTime, @@ -103,7 +118,7 @@ export const useEventsPoller = () => { } }, queryFn: () => getEvents({}, filter).then((data) => data.data), - queryKey: ['events', 'poller'], + queryKey: ['events', 'poller', hasFetchedInitialEvents], refetchInterval: pollingInterval, }); From 7f2911d4154bc92d03e21330b682873bd40f599c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 14 Dec 2023 13:53:30 -0500 Subject: [PATCH 19/44] fix incorrect query key causing `useInProgressEvents` not to work --- packages/manager/src/queries/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index fde4fcaff05..f67ff7cee35 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -49,7 +49,7 @@ export const useEventsInfiniteQuery = (filter?: Filter) => { export const useInProgressEvents = () => { return useQuery({ - queryKey: ['events', 'poller'], + queryKey: ['events', 'poller', true], }); }; From bd73b741185806b54366b04155fd66e79771e554 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 15 Dec 2023 14:43:42 -0500 Subject: [PATCH 20/44] add comments --- packages/manager/src/queries/events.ts | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index f67ff7cee35..532b6725409 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -23,6 +23,14 @@ import { generatePollingFilter } from 'src/utilities/requestFilters'; import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; +/** + * Gets an infinitly scrollable list of all Events + * + * This query is kept up to date by `useEventsPoller`. + * + * @param filter an optional filter can be passed to filter out events. If you use a filter, + * you must make sure `filteredEvents` implements the same filtering so the cache is updated correctly. + */ export const useEventsInfiniteQuery = (filter?: Filter) => { const query = useInfiniteQuery, APIError[]>( ['events', 'infinite', filter], @@ -47,12 +55,36 @@ export const useEventsInfiniteQuery = (filter?: Filter) => { return { ...query, events }; }; +/** + * A hook that allows you to access an `Event[]` of all in progress events. + * + * The benefit to using this hook is that it only returns in-progress events, + * which is generally a small number of items. This is good for performantly + * finding and rendering the status of an in-progress event. + * + * @example + * const { data: events } = useInProgressEvents(); + * + * const mostRecentLinodeEvent = events?.find( + * (e) => e.entity?.type === 'linode' && e.entity.id === id + * ); + * + * return ( + *

Linode In Progress Event: {event.percent_complete}

+ * ); + */ export const useInProgressEvents = () => { return useQuery({ queryKey: ['events', 'poller', true], }); }; +/** + * Mounting this hook will start polling the events endpoint and + * update our cache as new events come in. + * + * *Warning* This hook should only be mounted once! + */ export const useEventsPoller = () => { const { incrementPollingInterval, pollingInterval } = usePollingInterval(); @@ -125,6 +157,9 @@ export const useEventsPoller = () => { return null; }; +/** + * Manages the events polling interval. + */ export const usePollingInterval = () => { const queryKey = ['events', 'interval']; const queryClient = useQueryClient(); @@ -185,6 +220,13 @@ export const useMarkEventsAsSeen = () => { ); }; +/** + * For all infinite event queries (with any filter), update each infinite query in + * the cache. + * + * The catch here is that we must mimic the API filtering if we update an infinite query + * with an API filter. + */ export const updateEventsQueries = ( events: Event[], queryClient: QueryClient @@ -221,6 +263,13 @@ export const updateEventsQueries = ( }); }; +/** + * Updates a events infinite query with incoming events from our polling + * + * This method should do two things with incomming events + * - If the events is already in the cache, update it + * - If the event is new, append it to the top of the first page. + */ export const updateEventsQuery = ( events: Event[], queryKey: QueryKey, From 99dd84b238fe31a0ab68a49ac635bf3ac6770614 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 15 Dec 2023 15:24:56 -0500 Subject: [PATCH 21/44] comment and test filtering --- packages/manager/src/queries/events.test.ts | 50 +++++++++++++++++++++ packages/manager/src/queries/events.ts | 43 +++++++++++------- 2 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 packages/manager/src/queries/events.test.ts diff --git a/packages/manager/src/queries/events.test.ts b/packages/manager/src/queries/events.test.ts new file mode 100644 index 00000000000..9b15d07ae8b --- /dev/null +++ b/packages/manager/src/queries/events.test.ts @@ -0,0 +1,50 @@ +import { eventFactory } from 'src/factories'; + +import { doesEventMatchAPIFilter } from './events'; + +describe('doesEventMatchAPIFilter', () => { + it('should return false if the API filter filters out profile_update events', () => { + const event = eventFactory.build({ action: 'profile_update' }); + const filter = { action: { '+neq': 'profile_update' } }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(false); + }); + + it('should return false because this event does not meet the API filter criteria', () => { + const event = eventFactory.build({ + action: 'profile_update', + entity: null, + }); + const filter = { 'entity.id': 2, 'entity.type': 'linode' }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(false); + }); + + it('should return true because linode_boot would be allowed by this event', () => { + const event = eventFactory.build({ action: 'linode_boot' }); + const filter = { action: { '+neq': 'profile_update' } }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(true); + }); + + it('should return true because the incomming entity matches the API filter', () => { + const event = eventFactory.build({ entity: { id: 1, type: 'linode' } }); + const filter = { 'entity.id': 1, 'entity.type': 'linode' }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(true); + }); + + it('should return false because the incomming event does not match the API filter', () => { + const event = eventFactory.build({ entity: { id: 1, type: 'linode' } }); + const filter = { 'entity.id': 2, 'entity.type': 'linode' }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(false); + }); + + it('should return false because the incomming event does not match the API filter', () => { + const event = eventFactory.build({ entity: null }); + const filter = { 'entity.id': 2, 'entity.type': 'linode' }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(false); + }); +}); diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 532b6725409..8e12e30709f 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -29,7 +29,7 @@ import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; * This query is kept up to date by `useEventsPoller`. * * @param filter an optional filter can be passed to filter out events. If you use a filter, - * you must make sure `filteredEvents` implements the same filtering so the cache is updated correctly. + * you must make sure `doesEventMatchAPIFilter` implements the same filtering so the cache is updated correctly. */ export const useEventsInfiniteQuery = (filter?: Filter) => { const query = useInfiniteQuery, APIError[]>( @@ -243,26 +243,39 @@ export const updateEventsQueries = ( } const filteredEvents = events.filter((event) => { - // @ts-expect-error todo fix filter type - const notEqualItems = apiFilter.action?.['+neq']; - if (notEqualItems && notEqualItems.includes(event.action)) { - return false; - } - if ( - apiFilter?.['entity.id'] && - apiFilter?.['entity.type'] && - apiFilter['entity.id'] !== event.entity?.id && - apiFilter['entity.type'] !== event.entity?.type - ) { - return false; - } - return true; + return doesEventMatchAPIFilter(event, apiFilter); }); updateEventsQuery(filteredEvents, queryKey, queryClient); }); }; +/** + * Because we using one polling instance (without any API filter) and have many possible event infinite queires + * with various filters, we must make sure that we filter out API-filtered events when we update our filtered + * infinite queries. + * + * @returns This function return true if the API filter `filter` would match the given `event`. We are basiclly + * mimicing the API's filtering for the sake of updating our different events infinite queries. + */ +export const doesEventMatchAPIFilter = (event: Event, filter: Filter) => { + // @ts-expect-error todo fix filter type + const notEqualItems = filter.action?.['+neq']; + if (notEqualItems && notEqualItems.includes(event.action)) { + return false; + } + + if (filter?.['entity.id'] && filter['entity.id'] !== event.entity?.id) { + return false; + } + + if (filter?.['entity.type'] && filter['entity.type'] !== event.entity?.type) { + return false; + } + + return true; +}; + /** * Updates a events infinite query with incoming events from our polling * From 62ed2095745b39c5a02610c5e92e77ce40ebcdce Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 15 Dec 2023 15:42:44 -0500 Subject: [PATCH 22/44] explain pagination approach --- packages/manager/src/queries/events.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events.ts index 8e12e30709f..d04b135ea73 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events.ts @@ -30,6 +30,10 @@ import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; * * @param filter an optional filter can be passed to filter out events. If you use a filter, * you must make sure `doesEventMatchAPIFilter` implements the same filtering so the cache is updated correctly. + * + * The magic here is that we're doing cursor based pagination using the event `id`. + * We are doing this as opposed to page based pagination because we need an accurate way to get + * the next set of events when the items retrned by the server may have shifted. */ export const useEventsInfiniteQuery = (filter?: Filter) => { const query = useInfiniteQuery, APIError[]>( @@ -251,7 +255,7 @@ export const updateEventsQueries = ( }; /** - * Because we using one polling instance (without any API filter) and have many possible event infinite queires + * Because we're using one polling instance (without any API filter) and have many possible event infinite queires * with various filters, we must make sure that we filter out API-filtered events when we update our filtered * infinite queries. * From 94c0980de94112aaabf6ddb1cae23bde7ea6263c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 18 Dec 2023 15:24:02 -0500 Subject: [PATCH 23/44] clean up and test `EventsLanding` --- packages/manager/src/MainContent.tsx | 6 +- .../features/Events/EventsLanding.test.tsx | 75 ++++++++++++ .../src/features/Events/EventsLanding.tsx | 109 +++++++----------- packages/manager/src/features/Events/index.ts | 1 - .../LinodeActivity/LinodeActivity.tsx | 2 +- 5 files changed, 122 insertions(+), 71 deletions(-) create mode 100644 packages/manager/src/features/Events/EventsLanding.test.tsx delete mode 100644 packages/manager/src/features/Events/index.ts diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index c0594e97b78..b17e0e30a28 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -159,8 +159,10 @@ const Help = React.lazy(() => const SearchLanding = React.lazy( () => import('src/features/Search/SearchLanding') ); -const EventsLanding = React.lazy( - () => import('src/features/Events/EventsLanding') +const EventsLanding = React.lazy(() => + import('src/features/Events/EventsLanding').then((module) => ({ + default: module.EventsLanding, + })) ); const AccountActivationLanding = React.lazy( () => import('src/components/AccountActivation/AccountActivationLanding') diff --git a/packages/manager/src/features/Events/EventsLanding.test.tsx b/packages/manager/src/features/Events/EventsLanding.test.tsx new file mode 100644 index 00000000000..4250cda4427 --- /dev/null +++ b/packages/manager/src/features/Events/EventsLanding.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import { eventFactory } from 'src/factories'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { rest, server } from 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { EventsLanding } from './EventsLanding'; + +describe('EventsLanding', () => { + it('renders an empty state', async () => { + server.use( + rest.get('*/events', (req, res, ctx) => + res(ctx.json(makeResourcePage([]))) + ) + ); + + const { findByText } = renderWithTheme(); + + await findByText("You don't have any events on your account."); + }); + it('renders a custom empty state message', async () => { + server.use( + rest.get('*/events', (req, res, ctx) => + res(ctx.json(makeResourcePage([]))) + ) + ); + + const emptyMessage = 'No Linode Events :('; + + const { findByText } = renderWithTheme( + + ); + + await findByText(emptyMessage); + }); + + it('renders an event from the API', async () => { + const event = eventFactory.build({ + action: 'volume_create', + entity: { + label: 'my-volume', + type: 'volume', + }, + username: 'user', + }); + + server.use( + rest.get('*/events', (req, res, ctx) => + res(ctx.json(makeResourcePage([event]))) + ) + ); + + const { findByText } = renderWithTheme(); + + await findByText('my-volume'); + await findByText('is being created by user.', { exact: false }); + }); + + it('renders a message when there are no more events to load', async () => { + const event = eventFactory.build(); + + server.use( + rest.get('*/events', (req, res, ctx) => + res( + ctx.json(makeResourcePage([event], { page: 1, pages: 1, results: 1 })) + ) + ) + ); + + const { findByText } = renderWithTheme(); + + await findByText('No more events to show'); + }); +}); diff --git a/packages/manager/src/features/Events/EventsLanding.tsx b/packages/manager/src/features/Events/EventsLanding.tsx index 96751619454..73e3f5be91b 100644 --- a/packages/manager/src/features/Events/EventsLanding.tsx +++ b/packages/manager/src/features/Events/EventsLanding.tsx @@ -1,4 +1,3 @@ -import { Event } from '@linode/api-v4/lib/account'; import * as React from 'react'; import { Waypoint } from 'react-waypoint'; @@ -47,6 +46,46 @@ export const EventsLanding = (props: Props) => { isLoading, } = useEventsInfiniteQuery(filter); + const renderTableBody = () => { + if (isLoading) { + return ( + + ); + } else if (error) { + return ; + } else if (events && events.length === 0) { + return ( + + ); + } else { + return ( + <> + {events?.map((event) => ( + + ))} + {isFetchingNextPage && ( + + )} + + ); + } + }; + return ( <> {/* Only display this title on the main Events landing page */} @@ -57,9 +96,7 @@ export const EventsLanding = (props: Props) => { - - Event - + Event Relative Date @@ -68,16 +105,7 @@ export const EventsLanding = (props: Props) => { - - {renderTableBody( - isLoading, - isFetchingNextPage, - entityId, - error?.[0].reason, - events, - emptyMessage - )} - + {renderTableBody()} {hasNextPage ? ( fetchNextPage()}> @@ -92,56 +120,3 @@ export const EventsLanding = (props: Props) => { ); }; - -export const renderTableBody = ( - loading: boolean, - isRequesting: boolean, - entityId?: number, - error?: string, - events?: Event[], - emptyMessage = "You don't have any events on your account." -) => { - if (loading) { - return ( - - ); - } else if (error) { - return ( - - ); - } else if (events && events.length === 0) { - return ( - - ); - } else { - return ( - <> - {events?.map((thisEvent, idx) => ( - - ))} - {isRequesting && ( - - )} - - ); - } -}; - - -export default EventsLanding; diff --git a/packages/manager/src/features/Events/index.ts b/packages/manager/src/features/Events/index.ts deleted file mode 100644 index 2fdf42c8982..00000000000 --- a/packages/manager/src/features/Events/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as NotificationsLanding } from './EventsLanding'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.tsx index 9ea3ec4ec59..0547909441b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useParams } from 'react-router-dom'; -import EventsLanding from 'src/features/Events/EventsLanding'; +import { EventsLanding } from 'src/features/Events/EventsLanding'; const LinodeActivity = () => { const { linodeId } = useParams<{ linodeId: string }>(); From 9bbc27bc20b444cb8e71233347cf16671a89b36f Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 5 Jan 2024 14:27:37 -0500 Subject: [PATCH 24/44] remove `src/__data__/events.ts` --- packages/manager/src/__data__/events.ts | 596 ------------------ .../src/features/Events/Event.helpers.test.ts | 53 +- 2 files changed, 49 insertions(+), 600 deletions(-) delete mode 100644 packages/manager/src/__data__/events.ts diff --git a/packages/manager/src/__data__/events.ts b/packages/manager/src/__data__/events.ts deleted file mode 100644 index 9893388014a..00000000000 --- a/packages/manager/src/__data__/events.ts +++ /dev/null @@ -1,596 +0,0 @@ -import { Event } from '@linode/api-v4/lib/account'; - -export const events: Event[] = [ - { - action: 'linode_reboot', - created: '2018-12-03T22:37:20', - duration: 0, - entity: { - id: 11241778, - label: 'node-server', - type: 'linode', - url: '/v4/linode/instances/11241778', - }, - id: 18029754, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_reboot', - created: '2018-12-03T22:38:16', - duration: 0, - entity: { - id: 11241778, - label: 'node-server', - type: 'linode', - url: '/v4/linode/instances/11241778', - }, - id: 18029767, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_reboot', - created: '2018-12-03T22:34:09', - duration: 0, - entity: { - id: 11241778, - label: 'node-server', - type: 'linode', - url: '/v4/linode/instances/11241778', - }, - id: 18029572, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-03T19:59:53', - duration: 0, - entity: { - id: 11642886, - label: 'linode11642886', - type: 'linode', - url: '/v4/linode/instances/11642886', - }, - id: 18022171, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-03T19:54:28', - duration: 0, - entity: { - id: 11642886, - label: 'linode11642886', - type: 'linode', - url: '/v4/linode/instances/11642886', - }, - id: 18022021, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_reboot', - created: '2018-12-03T19:53:26', - duration: 0, - entity: { - id: 11241778, - label: 'node-server', - type: 'linode', - url: '/v4/linode/instances/11241778', - }, - id: 18021942, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T23:15:45', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17957943, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_delete', - created: '2018-12-02T23:15:45', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17957944, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T23:08:38', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17957718, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:55:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17957108, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T22:48:37', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17956743, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:38:36', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17956360, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T22:31:24', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17955994, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:29:02', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17955841, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T22:25:39', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17955781, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:23:58', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17955694, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T22:21:51', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17955636, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:20:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17955613, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T22:19:10', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17955464, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T22:03:38', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17954738, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T20:45:52', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17951319, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T20:39:36', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17951106, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_shutdown', - created: '2018-12-02T20:35:54', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17950945, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T20:23:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17950407, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'lassie_reboot', - created: '2018-12-02T20:21:11', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17950183, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, -]; - -export const uniqueEvents: Event[] = [ - { - action: 'linode_boot', - created: '2018-12-02T20:23:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 1231234, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T20:23:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17950407, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, -]; - -export const reduxEvent: Event = { - action: 'linode_boot', - created: '2018-12-02T20:23:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 123551234, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', -}; diff --git a/packages/manager/src/features/Events/Event.helpers.test.ts b/packages/manager/src/features/Events/Event.helpers.test.ts index 3567d837373..441010b199c 100644 --- a/packages/manager/src/features/Events/Event.helpers.test.ts +++ b/packages/manager/src/features/Events/Event.helpers.test.ts @@ -1,6 +1,3 @@ -import { EventAction } from '@linode/api-v4/lib/account'; - -import { reduxEvent, uniqueEvents } from 'src/__data__/events'; import { eventFactory } from 'src/factories/events'; import { @@ -12,6 +9,53 @@ import { shouldUpdateEvents, } from './Event.helpers'; +import type { Event, EventAction } from '@linode/api-v4'; + +const uniqueEvents: Event[] = [ + { + action: 'linode_boot', + created: '2018-12-02T20:23:43', + duration: 0, + entity: { + id: 11440645, + label: 'linode11440645', + type: 'linode', + url: '/v4/linode/instances/11440645', + }, + id: 1231234, + message: null, + percent_complete: 100, + rate: null, + read: false, + secondary_entity: null, + seen: true, + status: 'finished', + time_remaining: null, + username: 'test', + }, + { + action: 'linode_boot', + created: '2018-12-02T20:23:43', + duration: 0, + entity: { + id: 11440645, + label: 'linode11440645', + type: 'linode', + url: '/v4/linode/instances/11440645', + }, + id: 17950407, + message: null, + percent_complete: 100, + rate: null, + read: false, + secondary_entity: null, + seen: true, + status: 'finished', + time_remaining: null, + username: 'test', + }, +]; + const inProgressEvents = { 1234: 50, 1235: 20, @@ -26,7 +70,8 @@ const nextTime = '1556810370715'; describe('Utility Functions', () => { it('should filter out unique events', () => { - expect(filterUniqueEvents([reduxEvent, reduxEvent])).toHaveLength(1); + const mockEvent: Event = eventFactory.build(); + expect(filterUniqueEvents([mockEvent, mockEvent])).toHaveLength(1); expect(filterUniqueEvents(uniqueEvents)).toHaveLength(2); }); From 7d9db1d7c403a268979a1e55040a984729e23245 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 5 Jan 2024 14:49:52 -0500 Subject: [PATCH 25/44] remove more unused helper functions --- .../src/features/Events/Event.helpers.test.ts | 135 +----------------- .../src/features/Events/Event.helpers.ts | 71 --------- 2 files changed, 1 insertion(+), 205 deletions(-) diff --git a/packages/manager/src/features/Events/Event.helpers.test.ts b/packages/manager/src/features/Events/Event.helpers.test.ts index 441010b199c..cb2acd4fbd8 100644 --- a/packages/manager/src/features/Events/Event.helpers.test.ts +++ b/packages/manager/src/features/Events/Event.helpers.test.ts @@ -1,147 +1,14 @@ import { eventFactory } from 'src/factories/events'; import { - filterUniqueEvents, formatEventWithAppendedText, formatEventWithUsername, maybeRemoveTrailingPeriod, - percentCompleteHasUpdated, - shouldUpdateEvents, } from './Event.helpers'; -import type { Event, EventAction } from '@linode/api-v4'; - -const uniqueEvents: Event[] = [ - { - action: 'linode_boot', - created: '2018-12-02T20:23:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 1231234, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, - { - action: 'linode_boot', - created: '2018-12-02T20:23:43', - duration: 0, - entity: { - id: 11440645, - label: 'linode11440645', - type: 'linode', - url: '/v4/linode/instances/11440645', - }, - id: 17950407, - message: null, - percent_complete: 100, - rate: null, - read: false, - secondary_entity: null, - seen: true, - status: 'finished', - time_remaining: null, - username: 'test', - }, -]; - -const inProgressEvents = { - 1234: 50, - 1235: 20, -}; - -const nextInProgressEvents = { - 1234: 80, -}; - -const mostRecentEventTime = '1556810353941'; -const nextTime = '1556810370715'; +import type { EventAction } from '@linode/api-v4'; describe('Utility Functions', () => { - it('should filter out unique events', () => { - const mockEvent: Event = eventFactory.build(); - expect(filterUniqueEvents([mockEvent, mockEvent])).toHaveLength(1); - expect(filterUniqueEvents(uniqueEvents)).toHaveLength(2); - }); - - it('should return true if percent complete has changed', () => { - expect( - percentCompleteHasUpdated(inProgressEvents, inProgressEvents) - ).toBeFalsy(); - expect( - percentCompleteHasUpdated(inProgressEvents, nextInProgressEvents) - ).toBeTruthy(); - expect(percentCompleteHasUpdated(inProgressEvents, {})).toBeTruthy(); - }); - - it('should update events if most recent event time has changed', () => { - expect( - shouldUpdateEvents( - { - inProgressEvents, - mostRecentEventTime, - }, - { - inProgressEvents, - mostRecentEventTime: nextTime, - } - ) - ).toBeTruthy(); - }); - - it('should update if event progress has updated', () => { - expect( - shouldUpdateEvents( - { - inProgressEvents, - mostRecentEventTime, - }, - { - inProgressEvents: nextInProgressEvents, - mostRecentEventTime, - } - ) - ).toBeTruthy(); - expect( - shouldUpdateEvents( - { - inProgressEvents, - mostRecentEventTime, - }, - { - inProgressEvents: {}, - mostRecentEventTime, - } - ) - ).toBeTruthy(); - }); - - it('should not update if nothing has changed', () => { - expect( - shouldUpdateEvents( - { - inProgressEvents, - mostRecentEventTime, - }, - { - inProgressEvents, - mostRecentEventTime, - } - ) - ).toBeFalsy(); - }); - describe('formatEventWithUsername utility', () => { it('it should return a message for an event without a username unchanged', () => { const message = 'a message'; diff --git a/packages/manager/src/features/Events/Event.helpers.ts b/packages/manager/src/features/Events/Event.helpers.ts index 3968f8c8bc0..714206fb423 100644 --- a/packages/manager/src/features/Events/Event.helpers.ts +++ b/packages/manager/src/features/Events/Event.helpers.ts @@ -1,75 +1,4 @@ import { Event, EventAction } from '@linode/api-v4/lib/account'; -import { equals } from 'ramda'; - -/** - * The point of this function is to ensure we don't have an activity stream - * that looks like: - * - * Linode hello_world has been booted - * Linode hello_world has been created - * Linode hello_world is scheduled to be booted - * Linode hello_world is scheduled to be created - * - * Basically, we're creating a cache and only adding to the cache if the Event - * ID doesn't already exist in the cache. This ensures that "has been created" - * events will replace the "is scheduled to" events - */ -export const filterUniqueEvents = (events: Event[]) => { - return events.reduce((acc, event) => { - const foundEventInAcc = acc.some( - (eachAccumEvent: Event) => eachAccumEvent.id === event.id - ); - return foundEventInAcc ? acc : [...acc, event]; - }, []); -}; - -/** - * Takes in the inProgressEvents which are sourced from Redux. These are a key-value - * pair where the key is the ID of the event in progress and the value is the percent_complete - * So it ends up comparing the values similar to - * - * { - * 1234: 50 - * } - * - * and - * - * { - * 1234: 79 - * } - * - * the "50" and the "79" are the things being compared - */ -export const percentCompleteHasUpdated = ( - prevEventsInProgress: Record, - nextEventsInProgress: Record -) => { - return Object.keys(prevEventsInProgress).some( - (eachEventID) => - prevEventsInProgress[eachEventID] !== nextEventsInProgress[eachEventID] - ); -}; - -interface Payload { - inProgressEvents: Record; - mostRecentEventTime: string; -} - -/** - * shouldComponentUpdate logic to determine if the list of events should update - * - * This is abstracted because it's shared logic between the EventsLanding the Activity Summary - */ -export const shouldUpdateEvents = (prevProps: Payload, nextProps: Payload) => { - return ( - !equals(prevProps.mostRecentEventTime, nextProps.mostRecentEventTime) || - !equals(prevProps.inProgressEvents, nextProps.inProgressEvents) || - percentCompleteHasUpdated( - prevProps.inProgressEvents, - nextProps.inProgressEvents - ) - ); -}; export const maybeRemoveTrailingPeriod = (string: string) => { const lastChar = string[string.length - 1]; From 0a93db90897242bfbdd9326900d4424ff8ca6c9c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 5 Jan 2024 14:58:47 -0500 Subject: [PATCH 26/44] reorganize helper functions --- packages/manager/src/App.tsx | 2 +- .../src/containers/events.container.ts | 2 +- .../src/features/Events/EventsLanding.tsx | 2 +- .../Images/ImagesCreate/CreateImageTab.tsx | 2 +- .../src/features/Images/ImagesDrawer.tsx | 2 +- .../src/features/Images/ImagesLanding.tsx | 4 +- .../NodePoolsDisplay/NodeTable.tsx | 2 +- .../Linodes/CloneLanding/CloneLanding.tsx | 2 +- .../features/Linodes/LinodeEntityDetail.tsx | 2 +- .../ApiAwarenessModal/ApiAwarenessModal.tsx | 2 +- .../LinodeBackup/CancelBackupsDialog.tsx | 2 +- .../LinodeBackup/CaptureSnapshot.tsx | 2 +- .../LinodeBackup/EnableBackupsDialog.tsx | 2 +- .../LinodeBackup/RestoreToLinodeDrawer.tsx | 2 +- .../LinodeConfigs/BootConfigDialog.tsx | 2 +- .../LinodeRebuild/RebuildFromImage.tsx | 2 +- .../LinodeRebuild/RebuildFromStackScript.tsx | 2 +- .../LinodeRescue/BareMetalRescue.tsx | 2 +- .../LinodeRescue/StandardRescueDialog.tsx | 2 +- .../LinodeResize/LinodeResize.tsx | 2 +- .../LinodeSettingsDeletePanel.tsx | 2 +- .../LinodeStorage/CreateDiskDrawer.tsx | 2 +- .../LinodeStorage/LinodeDiskRow.tsx | 2 +- .../LinodeStorage/ResizeDiskDrawer.tsx | 2 +- .../MutationNotification.tsx | 2 +- .../LinodesLanding/DeleteLinodeDialog.tsx | 2 +- .../LinodesLanding/LinodeRow/LinodeRow.tsx | 2 +- .../Linodes/MigrateLinode/MigrateLinode.tsx | 7 ++- .../Linodes/PowerActionsDialogOrDrawer.tsx | 2 +- .../manager/src/features/Linodes/index.tsx | 2 +- .../src/features/Linodes/transitions.ts | 2 +- .../useEventNotifications.tsx | 4 +- .../NotificationMenu/NotificationMenu.tsx | 2 +- .../features/Volumes/AttachVolumeDrawer.tsx | 2 +- .../features/Volumes/CloneVolumeDrawer.tsx | 2 +- .../features/Volumes/DeleteVolumeDialog.tsx | 2 +- .../features/Volumes/DetachVolumeDialog.tsx | 2 +- .../features/Volumes/ResizeVolumeDrawer.tsx | 2 +- .../VolumeDrawer/LinodeVolumeAttachForm.tsx | 2 +- .../VolumeDrawer/LinodeVolumeCreateForm.tsx | 2 +- packages/manager/src/queries/events.test.ts | 50 ------------------- .../events/event.helpers.test.ts | 48 ++++++++++++++++++ .../events/event.helpers.ts | 28 ++++++++++- .../src/queries/{ => events}/events.ts | 31 ++---------- 44 files changed, 125 insertions(+), 121 deletions(-) delete mode 100644 packages/manager/src/queries/events.test.ts rename packages/manager/src/{store => queries}/events/event.helpers.test.ts (59%) rename packages/manager/src/{store => queries}/events/event.helpers.ts (66%) rename packages/manager/src/queries/{ => events}/events.ts (89%) diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index 886018af986..1247e6972c0 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -16,7 +16,7 @@ import { SplashScreen } from './components/SplashScreen'; import { useAdobeAnalytics } from './hooks/useAdobeAnalytics'; import { useInitialRequests } from './hooks/useInitialRequests'; import { useNewRelic } from './hooks/useNewRelic'; -import { useEventsPoller } from './queries/events'; +import { useEventsPoller } from './queries/events/events'; import { useSetupFeatureFlags } from './useSetupFeatureFlags'; // Ensure component's display name is 'App' diff --git a/packages/manager/src/containers/events.container.ts b/packages/manager/src/containers/events.container.ts index a769af42f60..07c8549cdbb 100644 --- a/packages/manager/src/containers/events.container.ts +++ b/packages/manager/src/containers/events.container.ts @@ -1,6 +1,6 @@ import React from 'react'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; export type WithPollingIntervalProps = ReturnType; diff --git a/packages/manager/src/features/Events/EventsLanding.tsx b/packages/manager/src/features/Events/EventsLanding.tsx index 73e3f5be91b..7486341c5cb 100644 --- a/packages/manager/src/features/Events/EventsLanding.tsx +++ b/packages/manager/src/features/Events/EventsLanding.tsx @@ -10,7 +10,7 @@ import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import { useEventsInfiniteQuery } from 'src/queries/events'; +import { useEventsInfiniteQuery } from 'src/queries/events/events'; import { EventRow } from './EventRow'; import { diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index 0699aa3353c..fe320457f06 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -18,7 +18,7 @@ import { Typography } from 'src/components/Typography'; import { DiskSelect } from 'src/features/Linodes/DiskSelect/DiskSelect'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useFlags } from 'src/hooks/useFlags'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useCreateImageMutation } from 'src/queries/images'; import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/Images/ImagesDrawer.tsx b/packages/manager/src/features/Images/ImagesDrawer.tsx index f643f534299..ff4bb9234f8 100644 --- a/packages/manager/src/features/Images/ImagesDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesDrawer.tsx @@ -23,7 +23,7 @@ import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { useImageAndLinodeGrantCheck } from './utils'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; const useStyles = makeStyles()((theme: Theme) => ({ actionPanel: { diff --git a/packages/manager/src/features/Images/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding.tsx index dccee84adaa..49d9b65514e 100644 --- a/packages/manager/src/features/Images/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding.tsx @@ -29,7 +29,7 @@ import { Typography } from 'src/components/Typography'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { listToItemsByID } from 'src/queries/base'; -import { useEventsInfiniteQuery } from 'src/queries/events'; +import { useEventsInfiniteQuery } from 'src/queries/events/events'; import { queryKey, removeImageFromCache, @@ -39,7 +39,7 @@ import { import { isEventImageUpload, isEventInProgressDiskImagize, -} from 'src/store/events/event.helpers'; +} from 'src/queries/events/event.helpers'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import ImageRow, { ImageWithEvent } from './ImageRow'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx index 99c172a7185..6b5ee353879 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx @@ -21,7 +21,7 @@ import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; import { Typography } from 'src/components/Typography'; import { transitionText } from 'src/features/Linodes/transitions'; -import { useInProgressEvents } from 'src/queries/events'; +import { useInProgressEvents } from 'src/queries/events/events'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { LinodeWithMaintenance } from 'src/utilities/linodes'; diff --git a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx index ba914ae09ba..d6dc56ce920 100644 --- a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx +++ b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx @@ -27,7 +27,7 @@ import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx index 0edd5ee6a33..25a61112f4b 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.tsx @@ -67,7 +67,7 @@ import type { LinodeType, } from '@linode/api-v4/lib/linodes/types'; import type { Subnet } from '@linode/api-v4/lib/vpcs'; -import { useInProgressEvents } from 'src/queries/events'; +import { useInProgressEvents } from 'src/queries/events/events'; interface LinodeEntityDetailProps { id: number; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx b/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx index dbef53f21de..3c5bffdc215 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/ApiAwarenessModal/ApiAwarenessModal.tsx @@ -13,7 +13,7 @@ import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { Typography } from 'src/components/Typography'; -import { useInProgressEvents } from 'src/queries/events'; +import { useInProgressEvents } from 'src/queries/events/events'; import { sendApiAwarenessClickEvent } from 'src/utilities/analytics'; import { generateCurlCommand } from 'src/utilities/generate-cURL'; import { generateCLICommand } from 'src/utilities/generate-cli'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx index 3924ed56fa5..128f1aa3239 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useLinodeBackupsCancelMutation } from 'src/queries/linodes/backups'; import { sendBackupsDisabledEvent } from 'src/utilities/analytics'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx index 4cf9a1d6007..25aee468f7d 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx @@ -10,7 +10,7 @@ import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useLinodeBackupSnapshotMutation } from 'src/queries/linodes/backups'; import { getErrorMap } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx index 39307e41b58..456c25f7e08 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx @@ -7,7 +7,7 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { Currency } from 'src/components/Currency'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useLinodeBackupsEnableMutation } from 'src/queries/linodes/backups'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useTypeQuery } from 'src/queries/types'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx index dfdcd0c680b..599cea29bf6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx @@ -11,7 +11,7 @@ import { FormControl } from 'src/components/FormControl'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { FormHelperText } from 'src/components/FormHelperText'; import { Notice } from 'src/components/Notice/Notice'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useLinodeBackupRestoreMutation } from 'src/queries/linodes/backups'; import { useAllLinodesQuery, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx index 109bc10214d..85e7f2db957 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useRebootLinodeMutation } from 'src/queries/linodes/linodes'; interface Props { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx index fec130b5f70..a3524e2f61e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx @@ -35,7 +35,7 @@ import { StyledGrid, StyledNotice, } from './RebuildFromImage.styles'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; interface Props { disabled: boolean; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx index 8dfc22d82f4..af84981755f 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx @@ -23,7 +23,7 @@ import { } from 'src/features/StackScripts/stackScriptUtils'; import { useStackScript } from 'src/hooks/useStackScript'; import { listToItemsByID } from 'src/queries/base'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useAllImagesQuery } from 'src/queries/images'; import { usePreferences } from 'src/queries/preferences'; import { filterImagesByType } from 'src/store/image/image.helpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx index 6ccad357414..3ce190acb08 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx index 7d2d2da31f6..930df106956 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx @@ -11,7 +11,7 @@ import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { usePrevious } from 'src/hooks/usePrevious'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeQuery, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx index dcb5578e2b1..27227e106fe 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx @@ -43,7 +43,7 @@ import { import { UnifiedMigrationPanel } from './LinodeResizeUnifiedMigrationPanel'; import type { ButtonProps } from 'src/components/Button/Button'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; interface Props { linodeId?: number; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx index 2c81618cf73..9a5a4ee5d3e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx @@ -6,7 +6,7 @@ import { Button } from 'src/components/Button/Button'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useDeleteLinodeMutation, useLinodeQuery, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx index 070649247b6..6d3fba970eb 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx @@ -24,7 +24,7 @@ import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import { ImageAndPassword } from '../LinodeSettings/ImageAndPassword'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; type FileSystem = 'ext3' | 'ext4' | 'initrd' | 'raw' | 'swap'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx index 69bc18d40e7..f68bfa0d38b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx @@ -7,7 +7,7 @@ import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { Hidden } from 'src/components/Hidden'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import { useInProgressEvents } from 'src/queries/events'; +import { useInProgressEvents } from 'src/queries/events/events'; import { LinodeDiskActionMenu } from './LinodeDiskActionMenu'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx index 484d5d635b7..c8f5ca49d22 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx @@ -14,7 +14,7 @@ import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { TextTooltip } from 'src/components/TextTooltip'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useAllLinodeDisksQuery, useLinodeDiskResizeMutation, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx index 23d681e8675..d6710b787f9 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; import { MBpsIntraDC } from 'src/constants'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useStartLinodeMutationMutation } from 'src/queries/linodes/actions'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx index 7c4b71924e2..5b320764309 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx @@ -6,7 +6,7 @@ import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToCo import { Typography } from 'src/components/Typography'; import { useFlags } from 'src/hooks/useFlags'; import { useAccount } from 'src/queries/account'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useDeleteLinodeMutation, diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx index 4a938e07b08..d89f5ea0ffd 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.tsx @@ -17,7 +17,7 @@ import { transitionText, } from 'src/features/Linodes/transitions'; import { notificationContext as _notificationContext } from 'src/features/NotificationCenter/NotificationContext'; -import { useInProgressEvents } from 'src/queries/events'; +import { useInProgressEvents } from 'src/queries/events/events'; import { useTypeQuery } from 'src/queries/types'; import { capitalizeAllWords } from 'src/utilities/capitalize'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; diff --git a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx index 7471d12995d..095ca324c88 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx @@ -18,7 +18,10 @@ import { useAccountAgreements, useMutateAccountAgreements, } from 'src/queries/accountAgreements'; -import { useInProgressEvents, usePollingInterval } from 'src/queries/events'; +import { + useInProgressEvents, + usePollingInterval, +} from 'src/queries/events/events'; import { useImageQuery } from 'src/queries/images'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { @@ -28,7 +31,7 @@ import { import { useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { useTypeQuery } from 'src/queries/types'; -import { isEventRelevantToLinode } from 'src/store/events/event.helpers'; +import { isEventRelevantToLinode } from 'src/queries/events/event.helpers'; import { sendMigrationInitiatedEvent } from 'src/utilities/analytics'; import { formatDate } from 'src/utilities/formatDate'; import { getGDPRDetails } from 'src/utilities/formatRegion'; diff --git a/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx b/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx index a5759e64a6b..9195c28721c 100644 --- a/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx +++ b/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx @@ -8,7 +8,7 @@ import Select from 'src/components/EnhancedSelect/Select'; import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useBootLinodeMutation, diff --git a/packages/manager/src/features/Linodes/index.tsx b/packages/manager/src/features/Linodes/index.tsx index 990a40f12d6..e7bd952a866 100644 --- a/packages/manager/src/features/Linodes/index.tsx +++ b/packages/manager/src/features/Linodes/index.tsx @@ -3,7 +3,7 @@ import { Redirect, Route, Switch } from 'react-router-dom'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; import { useAllAccountMaintenanceQuery } from 'src/queries/accountMaintenance'; -import { useInProgressEvents } from 'src/queries/events'; +import { useInProgressEvents } from 'src/queries/events/events'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { addMaintenanceToLinodes } from 'src/utilities/linodes'; import { linodesInTransition } from './transitions'; diff --git a/packages/manager/src/features/Linodes/transitions.ts b/packages/manager/src/features/Linodes/transitions.ts index 5013fe65c8e..38f2d02eab5 100644 --- a/packages/manager/src/features/Linodes/transitions.ts +++ b/packages/manager/src/features/Linodes/transitions.ts @@ -5,7 +5,7 @@ import { isInProgressEvent, isPrimaryEntity, isSecondaryEntity, -} from 'src/store/events/event.helpers'; +} from 'src/queries/events/event.helpers'; import { capitalizeAllWords } from 'src/utilities/capitalize'; export const transitionStatus = [ diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx b/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx index 1dc3769250d..fb65737e227 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx @@ -2,8 +2,8 @@ import { Event, EventAction } from '@linode/api-v4/lib/account/types'; import { partition } from 'ramda'; import * as React from 'react'; -import { useEventsInfiniteQuery } from 'src/queries/events'; -import { isInProgressEvent } from 'src/store/events/event.helpers'; +import { useEventsInfiniteQuery } from 'src/queries/events/events'; +import { isInProgressEvent } from 'src/queries/events/event.helpers'; import { removeBlocklistedEvents } from 'src/utilities/eventUtils'; import { notificationContext as _notificationContext } from '../NotificationContext'; diff --git a/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx b/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx index 05d8752a001..c4cf65b9b16 100644 --- a/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx +++ b/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx @@ -21,7 +21,7 @@ import Notifications from 'src/features/NotificationCenter/Notifications'; import { useDismissibleNotifications } from 'src/hooks/useDismissibleNotifications'; import { usePrevious } from 'src/hooks/usePrevious'; import { useNotificationsQuery } from 'src/queries/accountNotifications'; -import { useMarkEventsAsSeen } from 'src/queries/events'; +import { useMarkEventsAsSeen } from 'src/queries/events/events'; import { ThunkDispatch } from 'src/store/types'; import { omittedProps } from 'src/utilities/omittedProps'; diff --git a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx index 9b0eb6df983..4fc0ed854fd 100644 --- a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx @@ -15,7 +15,7 @@ import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useGrants } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; interface Props { onClose: () => void; diff --git a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx b/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx index c8e33406004..6f51aae19bf 100644 --- a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx @@ -8,7 +8,7 @@ import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useGrants } from 'src/queries/profile'; import { useCloneVolumeMutation } from 'src/queries/volumes'; import { diff --git a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx b/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx index fe1eeb13518..0b3da1373c7 100644 --- a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useDeleteVolumeMutation } from 'src/queries/volumes'; interface Props { diff --git a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx index aac6d40c1d3..d64f65f9b5d 100644 --- a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useDetachVolumeMutation } from 'src/queries/volumes'; diff --git a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx b/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx index 7eec1f3d030..6f6d2a402fe 100644 --- a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx @@ -16,7 +16,7 @@ import { import { PricePanel } from './VolumeDrawer/PricePanel'; import { SizeField } from './VolumeDrawer/SizeField'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; interface Props { onClose: () => void; diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx index 9e53874fc98..5382e37a516 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx @@ -7,7 +7,7 @@ import { number, object } from 'yup'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Notice } from 'src/components/Notice/Notice'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useGrants } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; import { diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx index 16973a7bb80..1813041e8cf 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx @@ -10,7 +10,7 @@ import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; import { MAX_VOLUME_SIZE } from 'src/constants'; -import { usePollingInterval } from 'src/queries/events'; +import { usePollingInterval } from 'src/queries/events/events'; import { useGrants, useProfile } from 'src/queries/profile'; import { useCreateVolumeMutation } from 'src/queries/volumes'; import { sendCreateVolumeEvent } from 'src/utilities/analytics'; diff --git a/packages/manager/src/queries/events.test.ts b/packages/manager/src/queries/events.test.ts deleted file mode 100644 index 9b15d07ae8b..00000000000 --- a/packages/manager/src/queries/events.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { eventFactory } from 'src/factories'; - -import { doesEventMatchAPIFilter } from './events'; - -describe('doesEventMatchAPIFilter', () => { - it('should return false if the API filter filters out profile_update events', () => { - const event = eventFactory.build({ action: 'profile_update' }); - const filter = { action: { '+neq': 'profile_update' } }; - - expect(doesEventMatchAPIFilter(event, filter)).toBe(false); - }); - - it('should return false because this event does not meet the API filter criteria', () => { - const event = eventFactory.build({ - action: 'profile_update', - entity: null, - }); - const filter = { 'entity.id': 2, 'entity.type': 'linode' }; - - expect(doesEventMatchAPIFilter(event, filter)).toBe(false); - }); - - it('should return true because linode_boot would be allowed by this event', () => { - const event = eventFactory.build({ action: 'linode_boot' }); - const filter = { action: { '+neq': 'profile_update' } }; - - expect(doesEventMatchAPIFilter(event, filter)).toBe(true); - }); - - it('should return true because the incomming entity matches the API filter', () => { - const event = eventFactory.build({ entity: { id: 1, type: 'linode' } }); - const filter = { 'entity.id': 1, 'entity.type': 'linode' }; - - expect(doesEventMatchAPIFilter(event, filter)).toBe(true); - }); - - it('should return false because the incomming event does not match the API filter', () => { - const event = eventFactory.build({ entity: { id: 1, type: 'linode' } }); - const filter = { 'entity.id': 2, 'entity.type': 'linode' }; - - expect(doesEventMatchAPIFilter(event, filter)).toBe(false); - }); - - it('should return false because the incomming event does not match the API filter', () => { - const event = eventFactory.build({ entity: null }); - const filter = { 'entity.id': 2, 'entity.type': 'linode' }; - - expect(doesEventMatchAPIFilter(event, filter)).toBe(false); - }); -}); diff --git a/packages/manager/src/store/events/event.helpers.test.ts b/packages/manager/src/queries/events/event.helpers.test.ts similarity index 59% rename from packages/manager/src/store/events/event.helpers.test.ts rename to packages/manager/src/queries/events/event.helpers.test.ts index b9d5cb50921..15e3124522f 100644 --- a/packages/manager/src/store/events/event.helpers.test.ts +++ b/packages/manager/src/queries/events/event.helpers.test.ts @@ -1,6 +1,7 @@ import { entityFactory, eventFactory } from 'src/factories/events'; import { + doesEventMatchAPIFilter, isEventRelevantToLinode, isEventRelevantToLinodeAsSecondaryEntity, isInProgressEvent, @@ -84,3 +85,50 @@ describe('isEventRelevantToLinodeAsSecondaryEntity', () => { ); }); }); + +describe('doesEventMatchAPIFilter', () => { + it('should return false if the API filter filters out profile_update events', () => { + const event = eventFactory.build({ action: 'profile_update' }); + const filter = { action: { '+neq': 'profile_update' } }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(false); + }); + + it('should return false because this event does not meet the API filter criteria', () => { + const event = eventFactory.build({ + action: 'profile_update', + entity: null, + }); + const filter = { 'entity.id': 2, 'entity.type': 'linode' }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(false); + }); + + it('should return true because linode_boot would be allowed by this event', () => { + const event = eventFactory.build({ action: 'linode_boot' }); + const filter = { action: { '+neq': 'profile_update' } }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(true); + }); + + it('should return true because the incomming entity matches the API filter', () => { + const event = eventFactory.build({ entity: { id: 1, type: 'linode' } }); + const filter = { 'entity.id': 1, 'entity.type': 'linode' }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(true); + }); + + it('should return false because the incomming event does not match the API filter', () => { + const event = eventFactory.build({ entity: { id: 1, type: 'linode' } }); + const filter = { 'entity.id': 2, 'entity.type': 'linode' }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(false); + }); + + it('should return false because the incomming event does not match the API filter', () => { + const event = eventFactory.build({ entity: null }); + const filter = { 'entity.id': 2, 'entity.type': 'linode' }; + + expect(doesEventMatchAPIFilter(event, filter)).toBe(false); + }); +}); diff --git a/packages/manager/src/store/events/event.helpers.ts b/packages/manager/src/queries/events/event.helpers.ts similarity index 66% rename from packages/manager/src/store/events/event.helpers.ts rename to packages/manager/src/queries/events/event.helpers.ts index 8bc8db0a6ef..33733d30ff3 100644 --- a/packages/manager/src/store/events/event.helpers.ts +++ b/packages/manager/src/queries/events/event.helpers.ts @@ -1,4 +1,4 @@ -import { Event, EventAction } from '@linode/api-v4/lib/account'; +import { Event, EventAction, Filter } from '@linode/api-v4'; export const isLongPendingEvent = (event: Event): boolean => { const { action, status } = event; @@ -51,3 +51,29 @@ export const eventActionsForLinodeAsSecondaryEntity: EventAction[] = [ ]; export const isEventRelevantToLinodeAsSecondaryEntity = (event: Event) => eventActionsForLinodeAsSecondaryEntity.includes(event?.action); + +/** + * Because we're using one polling instance (without any API filter) and have many possible event infinite queires + * with various filters, we must make sure that we filter out API-filtered events when we update our filtered + * infinite queries. + * + * @returns This function return true if the API filter `filter` would match the given `event`. We are basiclly + * mimicing the API's filtering for the sake of updating our different events infinite queries. + */ +export const doesEventMatchAPIFilter = (event: Event, filter: Filter) => { + // @ts-expect-error todo fix filter type + const notEqualItems = filter.action?.['+neq']; + if (notEqualItems && notEqualItems.includes(event.action)) { + return false; + } + + if (filter?.['entity.id'] && filter['entity.id'] !== event.entity?.id) { + return false; + } + + if (filter?.['entity.type'] && filter['entity.type'] !== event.entity?.type) { + return false; + } + + return true; +}; diff --git a/packages/manager/src/queries/events.ts b/packages/manager/src/queries/events/events.ts similarity index 89% rename from packages/manager/src/queries/events.ts rename to packages/manager/src/queries/events/events.ts index d04b135ea73..0748f1565d8 100644 --- a/packages/manager/src/queries/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -18,7 +18,10 @@ import { } from 'src/constants'; import { useEventHandlers } from 'src/hooks/useEventHandlers'; import { useToastNotifications } from 'src/hooks/useToastNotifications'; -import { isInProgressEvent } from 'src/store/events/event.helpers'; +import { + doesEventMatchAPIFilter, + isInProgressEvent, +} from 'src/queries/events/event.helpers'; import { generatePollingFilter } from 'src/utilities/requestFilters'; import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; @@ -254,32 +257,6 @@ export const updateEventsQueries = ( }); }; -/** - * Because we're using one polling instance (without any API filter) and have many possible event infinite queires - * with various filters, we must make sure that we filter out API-filtered events when we update our filtered - * infinite queries. - * - * @returns This function return true if the API filter `filter` would match the given `event`. We are basiclly - * mimicing the API's filtering for the sake of updating our different events infinite queries. - */ -export const doesEventMatchAPIFilter = (event: Event, filter: Filter) => { - // @ts-expect-error todo fix filter type - const notEqualItems = filter.action?.['+neq']; - if (notEqualItems && notEqualItems.includes(event.action)) { - return false; - } - - if (filter?.['entity.id'] && filter['entity.id'] !== event.entity?.id) { - return false; - } - - if (filter?.['entity.type'] && filter['entity.type'] !== event.entity?.type) { - return false; - } - - return true; -}; - /** * Updates a events infinite query with incoming events from our polling * From 4d83772603abd8306bd9a24e12089d2dafa48c61 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 5 Jan 2024 15:02:34 -0500 Subject: [PATCH 27/44] move `generatePollingFilter` to `event.helpers.ts` --- .../src/queries/events/event.helpers.test.ts | 76 ++++++++++++++++++ .../src/queries/events/event.helpers.ts | 49 ++++++++++++ packages/manager/src/queries/events/events.ts | 2 +- .../src/utilities/requestFilters.test.ts | 78 ------------------- .../manager/src/utilities/requestFilters.ts | 50 ------------ 5 files changed, 126 insertions(+), 129 deletions(-) delete mode 100644 packages/manager/src/utilities/requestFilters.test.ts delete mode 100644 packages/manager/src/utilities/requestFilters.ts diff --git a/packages/manager/src/queries/events/event.helpers.test.ts b/packages/manager/src/queries/events/event.helpers.test.ts index 15e3124522f..4b8f6829714 100644 --- a/packages/manager/src/queries/events/event.helpers.test.ts +++ b/packages/manager/src/queries/events/event.helpers.test.ts @@ -2,6 +2,9 @@ import { entityFactory, eventFactory } from 'src/factories/events'; import { doesEventMatchAPIFilter, + generateInFilter, + generateNeqFilter, + generatePollingFilter, isEventRelevantToLinode, isEventRelevantToLinodeAsSecondaryEntity, isInProgressEvent, @@ -132,3 +135,76 @@ describe('doesEventMatchAPIFilter', () => { expect(doesEventMatchAPIFilter(event, filter)).toBe(false); }); }); + +describe('requestFilters', () => { + describe('generateInFilter', () => { + it('generates a filter from an array of values', () => { + const result = generateInFilter('id', [1, 2, 3]); + expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); + }); + }); + + describe('generateNeqFilter', () => { + it('generates +neq filter from key and array of values', () => { + const result = generateNeqFilter('id', [1, 2, 3]); + expect(result).toEqual([ + { id: { '+neq': 1 } }, + { id: { '+neq': 2 } }, + { id: { '+neq': 3 } }, + ]); + }); + }); + + describe('generatePollingFilter', () => { + const timestamp = '2020-01-01T00:00:00'; + + it('generates a simple filter when pollIDs is empty', () => { + const result = generatePollingFilter(timestamp, []); + expect(result).toEqual({ created: { '+gte': timestamp } }); + }); + + it('handles "in" IDs', () => { + const inIds = [1, 2, 3]; + const result = generatePollingFilter(timestamp, inIds); + expect(result).toEqual({ + '+or': [ + { created: { '+gte': timestamp } }, + { id: 1 }, + { id: 2 }, + { id: 3 }, + ], + }); + }); + + it('handles "+neq" IDs', () => { + const result = generatePollingFilter(timestamp, [], [1, 2, 3]); + expect(result).toEqual({ + '+and': [ + { created: { '+gte': timestamp } }, + { id: { '+neq': 1 } }, + { id: { '+neq': 2 } }, + { id: { '+neq': 3 } }, + ], + }); + }); + + it('handles "in" and "+neq" IDs together', () => { + const result = generatePollingFilter(timestamp, [1, 2, 3], [4, 5, 6]); + expect(result).toEqual({ + '+or': [ + { + '+and': [ + { created: { '+gte': timestamp } }, + { id: { '+neq': 4 } }, + { id: { '+neq': 5 } }, + { id: { '+neq': 6 } }, + ], + }, + { id: 1 }, + { id: 2 }, + { id: 3 }, + ], + }); + }); + }); +}); diff --git a/packages/manager/src/queries/events/event.helpers.ts b/packages/manager/src/queries/events/event.helpers.ts index 33733d30ff3..81362240a9c 100644 --- a/packages/manager/src/queries/events/event.helpers.ts +++ b/packages/manager/src/queries/events/event.helpers.ts @@ -77,3 +77,52 @@ export const doesEventMatchAPIFilter = (event: Event, filter: Filter) => { return true; }; + +/** + * Generates a "found in filter" list filter for the API. + * @example + * generateInFilter('id', [1, 22, 333, 4444]); + * would produce { '+or': [1, 22, 333, 4444] } + * and reads as "where `id` is 1, 22, 333, or 4444." + */ +export const generateInFilter = (keyName: string, arr: (number | string)[]) => { + return arr.map((el) => ({ [keyName]: el })); +}; + +export const generateNeqFilter = ( + keyName: string, + arr: (number | string)[] +) => { + return arr.map((el) => ({ [keyName]: { '+neq': el } })); +}; + +/** + * Generates a filter for API requests; + * If we have IDs: + * "If `created` is greater than the timestamp provided or the `id` is one of ids." + * or: + * "If `created` is greater than the timestamp provided." + * + * This filter is invoked on every events request to only get the latest or in-progress events. + */ +export const generatePollingFilter = ( + timestamp: string, + inIds: number[] = [], + neqIds: number[] = [] +) => { + let filter: Filter = { created: { '+gte': timestamp } }; + + if (neqIds.length > 0) { + filter = { + '+and': [filter, ...generateNeqFilter('id', neqIds)], + }; + } + + if (inIds.length > 0) { + filter = { + '+or': [filter, ...generateInFilter('id', inIds)], + }; + } + + return filter; +}; diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 0748f1565d8..8ec978fcd63 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -20,9 +20,9 @@ import { useEventHandlers } from 'src/hooks/useEventHandlers'; import { useToastNotifications } from 'src/hooks/useToastNotifications'; import { doesEventMatchAPIFilter, + generatePollingFilter, isInProgressEvent, } from 'src/queries/events/event.helpers'; -import { generatePollingFilter } from 'src/utilities/requestFilters'; import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; diff --git a/packages/manager/src/utilities/requestFilters.test.ts b/packages/manager/src/utilities/requestFilters.test.ts deleted file mode 100644 index 3ceac2298d1..00000000000 --- a/packages/manager/src/utilities/requestFilters.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - generateInFilter, - generateNeqFilter, - generatePollingFilter, -} from './requestFilters'; - -describe('requestFilters', () => { - describe('generateInFilter', () => { - it('generates a filter from an array of values', () => { - const result = generateInFilter('id', [1, 2, 3]); - expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); - }); - }); - - describe('generateNeqFilter', () => { - it('generates +neq filter from key and array of values', () => { - const result = generateNeqFilter('id', [1, 2, 3]); - expect(result).toEqual([ - { id: { '+neq': 1 } }, - { id: { '+neq': 2 } }, - { id: { '+neq': 3 } }, - ]); - }); - }); - - describe('generatePollingFilter', () => { - const timestamp = '2020-01-01T00:00:00'; - - it('generates a simple filter when pollIDs is empty', () => { - const result = generatePollingFilter(timestamp, []); - expect(result).toEqual({ created: { '+gte': timestamp } }); - }); - - it('handles "in" IDs', () => { - const inIds = [1, 2, 3]; - const result = generatePollingFilter(timestamp, inIds); - expect(result).toEqual({ - '+or': [ - { created: { '+gte': timestamp } }, - { id: 1 }, - { id: 2 }, - { id: 3 }, - ], - }); - }); - - it('handles "+neq" IDs', () => { - const result = generatePollingFilter(timestamp, [], [1, 2, 3]); - expect(result).toEqual({ - '+and': [ - { created: { '+gte': timestamp } }, - { id: { '+neq': 1 } }, - { id: { '+neq': 2 } }, - { id: { '+neq': 3 } }, - ], - }); - }); - - it('handles "in" and "+neq" IDs together', () => { - const result = generatePollingFilter(timestamp, [1, 2, 3], [4, 5, 6]); - expect(result).toEqual({ - '+or': [ - { - '+and': [ - { created: { '+gte': timestamp } }, - { id: { '+neq': 4 } }, - { id: { '+neq': 5 } }, - { id: { '+neq': 6 } }, - ], - }, - { id: 1 }, - { id: 2 }, - { id: 3 }, - ], - }); - }); - }); -}); diff --git a/packages/manager/src/utilities/requestFilters.ts b/packages/manager/src/utilities/requestFilters.ts deleted file mode 100644 index dfe52233fa0..00000000000 --- a/packages/manager/src/utilities/requestFilters.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Filter } from '@linode/api-v4'; - -/** - * Generates a "found in filter" list filter for the API. - * @example - * generateInFilter('id', [1, 22, 333, 4444]); - * would produce { '+or': [1, 22, 333, 4444] } - * and reads as "where `id` is 1, 22, 333, or 4444." - */ -export const generateInFilter = (keyName: string, arr: (number | string)[]) => { - return arr.map((el) => ({ [keyName]: el })); -}; - -export const generateNeqFilter = ( - keyName: string, - arr: (number | string)[] -) => { - return arr.map((el) => ({ [keyName]: { '+neq': el } })); -}; - -/** - * Generates a filter for API requests; - * If we have IDs: - * "If `created` is greater than the timestamp provided or the `id` is one of ids." - * or: - * "If `created` is greater than the timestamp provided." - * - * This filter is invoked on every events request to only get the latest or in-progress events. - */ -export const generatePollingFilter = ( - timestamp: string, - inIds: number[] = [], - neqIds: number[] = [] -) => { - let filter: Filter = { created: { '+gte': timestamp } }; - - if (neqIds.length > 0) { - filter = { - '+and': [filter, ...generateNeqFilter('id', neqIds)], - }; - } - - if (inIds.length > 0) { - filter = { - '+or': [filter, ...generateInFilter('id', inIds)], - }; - } - - return filter; -}; From 99218bd8aa9983acc1a500d9cda22571c8ff2332 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 5 Jan 2024 15:52:26 -0500 Subject: [PATCH 28/44] extract helper into `getExistingEventDataForPollingFilterGenerator` and unit test --- .../src/queries/events/event.helpers.test.ts | 45 ++++++++++++++++ .../src/queries/events/event.helpers.ts | 52 +++++++++++++++++++ packages/manager/src/queries/events/events.ts | 20 +------ 3 files changed, 99 insertions(+), 18 deletions(-) diff --git a/packages/manager/src/queries/events/event.helpers.test.ts b/packages/manager/src/queries/events/event.helpers.test.ts index 4b8f6829714..9308351a005 100644 --- a/packages/manager/src/queries/events/event.helpers.test.ts +++ b/packages/manager/src/queries/events/event.helpers.test.ts @@ -5,6 +5,7 @@ import { generateInFilter, generateNeqFilter, generatePollingFilter, + getExistingEventDataForPollingFilterGenerator, isEventRelevantToLinode, isEventRelevantToLinodeAsSecondaryEntity, isInProgressEvent, @@ -208,3 +209,47 @@ describe('requestFilters', () => { }); }); }); + +describe('getExistingEventDataForPollingFilterGenerator', () => { + it('returns the IDs in-progress of events', () => { + const inProgressEvent = eventFactory.build({ + percent_complete: 5, + status: 'started', + }); + const eventThatIsNotInProgress = eventFactory.build({ + percent_complete: null, + status: 'finished', + }); + + const { inProgressEvents } = getExistingEventDataForPollingFilterGenerator( + [eventThatIsNotInProgress, inProgressEvent], + '' + ); + + expect(inProgressEvents).toHaveLength(1); + expect(inProgressEvents[0]).toBe(inProgressEvent.id); + }); + + it('returns the ID of events with the same timestamp', () => { + const timestamp = '2024-01-05T19:28:19'; + + const eventWithSameTimestamp = eventFactory.build({ + created: timestamp, + }); + const eventWithDifferentTimestamp = eventFactory.build({ + created: '2024-01-01T18:28:19', + }); + + const { + eventsThatAlreadyHappenedAtTheFilterTime, + } = getExistingEventDataForPollingFilterGenerator( + [eventWithDifferentTimestamp, eventWithSameTimestamp], + timestamp + ); + + expect(eventsThatAlreadyHappenedAtTheFilterTime).toHaveLength(1); + expect(eventsThatAlreadyHappenedAtTheFilterTime[0]).toBe( + eventWithSameTimestamp.id + ); + }); +}); diff --git a/packages/manager/src/queries/events/event.helpers.ts b/packages/manager/src/queries/events/event.helpers.ts index 81362240a9c..433d0ebb6f2 100644 --- a/packages/manager/src/queries/events/event.helpers.ts +++ b/packages/manager/src/queries/events/event.helpers.ts @@ -126,3 +126,55 @@ export const generatePollingFilter = ( return filter; }; + +/** + * This function exists to... + * - Solve the "same second problem" + * - Continue to poll in-progress events + * + * ## The "same second problem" + * + * It's possible that we may poll for events and a new event might be created after we polled, + * but at the same second. We must overlap our requests so that we catch any "same second" events. + * This function returns the ID of events that happened at `latestEventTime` so that we can + * exclude them (using '+neq') them in our next request. + * + * + * ## Continue to poll in-progress events + * + * Given all events that have been fetched since the app loaded, + * this function returns the IDs of any in progress events so that we + * continue to poll them for updates. + * + * @param events All events that have been fetched since loading the app + * @param latestEventTime The created timestamp of the most revent event (for example: 2020-01-01T12:00:00) + */ +export const getExistingEventDataForPollingFilterGenerator = ( + events: Event[] | undefined, + latestEventTime: string +) => { + if (!events) { + return { + eventsThatAlreadyHappenedAtTheFilterTime: [], + inProgressEvents: [], + }; + } + + return events.reduce<{ + eventsThatAlreadyHappenedAtTheFilterTime: number[]; + inProgressEvents: number[]; + }>( + (acc, event) => { + if (event.created === latestEventTime) { + acc.eventsThatAlreadyHappenedAtTheFilterTime.push(event.id); + return acc; + } + if (isInProgressEvent(event)) { + acc.inProgressEvents.push(event.id); + return acc; + } + return acc; + }, + { eventsThatAlreadyHappenedAtTheFilterTime: [], inProgressEvents: [] } + ); +}; diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 8ec978fcd63..961dc979a56 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -21,7 +21,7 @@ import { useToastNotifications } from 'src/hooks/useToastNotifications'; import { doesEventMatchAPIFilter, generatePollingFilter, - isInProgressEvent, + getExistingEventDataForPollingFilterGenerator, } from 'src/queries/events/event.helpers'; import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; @@ -116,23 +116,7 @@ export const useEventsPoller = () => { const { eventsThatAlreadyHappenedAtTheFilterTime, inProgressEvents, - } = events?.reduce<{ - eventsThatAlreadyHappenedAtTheFilterTime: number[]; - inProgressEvents: number[]; - }>( - (acc, event) => { - if (isInProgressEvent(event)) { - acc.inProgressEvents.push(event.id); - return acc; - } - if (event.created === latestEventTime) { - acc.eventsThatAlreadyHappenedAtTheFilterTime.push(event.id); - return acc; - } - return acc; - }, - { eventsThatAlreadyHappenedAtTheFilterTime: [], inProgressEvents: [] } - ) ?? { eventsThatAlreadyHappenedAtTheFilterTime: [], inProgressEvents: [] }; + } = getExistingEventDataForPollingFilterGenerator(events, latestEventTime); const hasFetchedInitialEvents = events !== undefined; From d76f7e841002d41219cc212fbb277c8ae8a1a431 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 5 Jan 2024 16:43:22 -0500 Subject: [PATCH 29/44] handle and test in-progress event edge case --- .../src/queries/events/event.helpers.test.ts | 28 ++++++++++++++++++- .../src/queries/events/event.helpers.ts | 8 +++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/queries/events/event.helpers.test.ts b/packages/manager/src/queries/events/event.helpers.test.ts index 9308351a005..3c1221a8fd8 100644 --- a/packages/manager/src/queries/events/event.helpers.test.ts +++ b/packages/manager/src/queries/events/event.helpers.test.ts @@ -230,14 +230,18 @@ describe('getExistingEventDataForPollingFilterGenerator', () => { expect(inProgressEvents[0]).toBe(inProgressEvent.id); }); - it('returns the ID of events with the same timestamp', () => { + it('returns the IDs of events with the same timestamp', () => { const timestamp = '2024-01-05T19:28:19'; const eventWithSameTimestamp = eventFactory.build({ created: timestamp, + percent_complete: null, + status: 'finished', }); const eventWithDifferentTimestamp = eventFactory.build({ created: '2024-01-01T18:28:19', + percent_complete: null, + status: 'finished', }); const { @@ -252,4 +256,26 @@ describe('getExistingEventDataForPollingFilterGenerator', () => { eventWithSameTimestamp.id ); }); + + it('will return an in-progress event in `inProgressEvents` and not in `eventsThatAlreadyHappenedAtTheFilterTime` even if it happened same second', () => { + // we want this behavior because we want to keep polling in-progress events always + const timestamp = '2024-01-05T19:28:19'; + + const eventWithSameTimestamp = eventFactory.build({ + created: timestamp, + percent_complete: 5, + status: 'started', + }); + + const { + eventsThatAlreadyHappenedAtTheFilterTime, + inProgressEvents, + } = getExistingEventDataForPollingFilterGenerator( + [eventWithSameTimestamp], + timestamp + ); + + expect(eventsThatAlreadyHappenedAtTheFilterTime).toHaveLength(0); + expect(inProgressEvents).toHaveLength(1); + }); }); diff --git a/packages/manager/src/queries/events/event.helpers.ts b/packages/manager/src/queries/events/event.helpers.ts index 433d0ebb6f2..29c6dd979c1 100644 --- a/packages/manager/src/queries/events/event.helpers.ts +++ b/packages/manager/src/queries/events/event.helpers.ts @@ -165,14 +165,14 @@ export const getExistingEventDataForPollingFilterGenerator = ( inProgressEvents: number[]; }>( (acc, event) => { - if (event.created === latestEventTime) { - acc.eventsThatAlreadyHappenedAtTheFilterTime.push(event.id); - return acc; - } if (isInProgressEvent(event)) { acc.inProgressEvents.push(event.id); return acc; } + if (event.created === latestEventTime) { + acc.eventsThatAlreadyHappenedAtTheFilterTime.push(event.id); + return acc; + } return acc; }, { eventsThatAlreadyHappenedAtTheFilterTime: [], inProgressEvents: [] } From c003474acd5c101653de4d5b048994d465a25eef Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 11 Jan 2024 16:58:36 -0500 Subject: [PATCH 30/44] remove outdated comment --- packages/manager/src/store/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/manager/src/store/index.ts b/packages/manager/src/store/index.ts index 1daf1522c69..2728fe0b015 100644 --- a/packages/manager/src/store/index.ts +++ b/packages/manager/src/store/index.ts @@ -71,7 +71,6 @@ const reducers = combineReducers({ stackScriptDialog, }); -// We need an instance of the query client for some event event handlers export const storeFactory = () => createStore(reducers, defaultState, applyMiddleware(thunk)); From 7675ec5c1eb07d5de807866461463ccd9914af9f Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 17 Jan 2024 11:32:26 -0500 Subject: [PATCH 31/44] don't pass an empty object if we don't have a `pageParam` --- packages/manager/src/queries/events/events.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 961dc979a56..852db9e8f73 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -41,7 +41,11 @@ import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; export const useEventsInfiniteQuery = (filter?: Filter) => { const query = useInfiniteQuery, APIError[]>( ['events', 'infinite', filter], - ({ pageParam }) => getEvents({}, { ...filter, id: { '+lt': pageParam } }), + ({ pageParam }) => + getEvents( + {}, + { ...filter, id: pageParam ? { '+lt': pageParam } : undefined } + ), { cacheTime: Infinity, getNextPageParam: ({ data, results }) => { From 7bf1c849e52a5ae487563768f63fba0fdbd2ef02 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 17 Jan 2024 16:28:46 -0500 Subject: [PATCH 32/44] improve interval defaults --- packages/manager/src/queries/events/events.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 852db9e8f73..14a97302bd3 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -158,9 +158,11 @@ export const useEventsPoller = () => { export const usePollingInterval = () => { const queryKey = ['events', 'interval']; const queryClient = useQueryClient(); - const { data: intervalMultiplier = 1 } = useQuery(queryKey, () => - queryClient.getQueryData(queryKey) - ); + const { data: intervalMultiplier = 1 } = useQuery({ + enabled: false, + initialData: 1, + queryKey, + }); return { incrementPollingInterval: () => queryClient.setQueryData( From e4a9efeb6662c573eff30070759420c6b8412a5e Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 17 Jan 2024 16:48:20 -0500 Subject: [PATCH 33/44] disable query that is for reading data only --- packages/manager/src/queries/events/events.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 14a97302bd3..04a9c21f20a 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -86,6 +86,7 @@ export const useEventsInfiniteQuery = (filter?: Filter) => { */ export const useInProgressEvents = () => { return useQuery({ + enabled: false, queryKey: ['events', 'poller', true], }); }; From ff3b051999e070b2d0bf019909f87defccdde737 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 19 Jan 2024 10:13:35 -0500 Subject: [PATCH 34/44] make polling backoff match existing backoff --- packages/manager/src/queries/events/events.ts | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 04a9c21f20a..cd90a9f36e3 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -153,27 +153,56 @@ export const useEventsPoller = () => { return null; }; +const pollingIntervalQueryKey = ['events', 'interval']; + /** - * Manages the events polling interval. + * Manages and exposes the events polling interval. */ export const usePollingInterval = () => { - const queryKey = ['events', 'interval']; const queryClient = useQueryClient(); const { data: intervalMultiplier = 1 } = useQuery({ enabled: false, initialData: 1, - queryKey, + queryKey: pollingIntervalQueryKey, }); return { + /** + * Increases the polling interval by 1 second up to 16 seconds + */ incrementPollingInterval: () => queryClient.setQueryData( - queryKey, - Math.min(intervalMultiplier + 1, 16) + pollingIntervalQueryKey, + Math.min(intervalMultiplier * 2, 16) ), pollingInterval: DISABLE_EVENT_THROTTLE ? 500 : intervalMultiplier * INTERVAL, - resetEventsPolling: () => queryClient.setQueryData(queryKey, 1), + resetEventsPolling: () => + queryClient.setQueryData(pollingIntervalQueryKey, 1), + }; +}; + +/** + * Manages the events polling interval. + * + * This hook should be used in application components that need to change + * the events polling interval. + */ +export const useEventsPollingActions = () => { + const queryClient = useQueryClient(); + + const resetEventsPolling = queryClient.setQueryData( + pollingIntervalQueryKey, + 1 + ); + + return { + /** + * Sets the polling interval to 1 second so that events get polled faster temporarily + * + * The polling backoff will start over from 1 second. + */ + resetEventsPolling, }; }; From 319784652a45ba4ea5d788e3882a7eda8e4957e4 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 19 Jan 2024 10:48:32 -0500 Subject: [PATCH 35/44] clean up interval hooks --- packages/manager/src/containers/events.container.ts | 12 +++++++----- .../features/Images/ImagesCreate/CreateImageTab.tsx | 6 +++--- .../manager/src/features/Images/ImagesDrawer.tsx | 6 +++--- .../features/Linodes/CloneLanding/CloneLanding.tsx | 4 ++-- .../Linodes/LinodesCreate/LinodeCreateContainer.tsx | 11 +++++++---- .../LinodeBackup/CancelBackupsDialog.tsx | 4 ++-- .../LinodesDetail/LinodeBackup/CaptureSnapshot.tsx | 4 ++-- .../LinodeBackup/EnableBackupsDialog.tsx | 4 ++-- .../LinodeBackup/RestoreToLinodeDrawer.tsx | 4 ++-- .../LinodesDetail/LinodeConfigs/BootConfigDialog.tsx | 4 ++-- .../LinodesDetail/LinodeRebuild/RebuildFromImage.tsx | 4 ++-- .../LinodeRebuild/RebuildFromStackScript.tsx | 4 ++-- .../LinodesDetail/LinodeRescue/BareMetalRescue.tsx | 4 ++-- .../LinodeRescue/StandardRescueDialog.tsx | 4 ++-- .../LinodesDetail/LinodeResize/LinodeResize.tsx | 4 ++-- .../LinodeSettings/LinodeSettingsDeletePanel.tsx | 4 ++-- .../LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx | 4 ++-- .../LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx | 4 ++-- .../LinodesDetailHeader/MutationNotification.tsx | 4 ++-- .../Linodes/LinodesLanding/DeleteLinodeDialog.tsx | 4 ++-- .../features/Linodes/MigrateLinode/MigrateLinode.tsx | 6 +++--- .../features/Linodes/PowerActionsDialogOrDrawer.tsx | 4 ++-- .../src/features/Volumes/AttachVolumeDrawer.tsx | 4 ++-- .../src/features/Volumes/CloneVolumeDrawer.tsx | 4 ++-- .../src/features/Volumes/DeleteVolumeDialog.tsx | 4 ++-- .../src/features/Volumes/DetachVolumeDialog.tsx | 4 ++-- .../src/features/Volumes/ResizeVolumeDrawer.tsx | 4 ++-- .../Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx | 4 ++-- .../Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx | 4 ++-- packages/manager/src/queries/events/events.ts | 11 +++++------ 30 files changed, 76 insertions(+), 72 deletions(-) diff --git a/packages/manager/src/containers/events.container.ts b/packages/manager/src/containers/events.container.ts index 07c8549cdbb..37ffae66821 100644 --- a/packages/manager/src/containers/events.container.ts +++ b/packages/manager/src/containers/events.container.ts @@ -1,14 +1,16 @@ import React from 'react'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; -export type WithPollingIntervalProps = ReturnType; +export type WithEventsPollingActionProps = ReturnType< + typeof useEventsPollingActions +>; -export const withPollingInterval = ( - Component: React.ComponentType +export const withEventsPollingActions = ( + Component: React.ComponentType ) => { return (props: Props) => { - const polling = usePollingInterval(); + const polling = useEventsPollingActions(); return React.createElement(Component, { ...props, diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index fe320457f06..c72e40153a9 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -1,11 +1,11 @@ import { Disk, Linode, getLinodeDisks } from '@linode/api-v4/lib/linodes'; import { APIError } from '@linode/api-v4/lib/types'; import { Theme } from '@mui/material/styles'; -import { makeStyles } from 'tss-react/mui'; import { useSnackbar } from 'notistack'; import { equals } from 'ramda'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; +import { makeStyles } from 'tss-react/mui'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; @@ -18,7 +18,7 @@ import { Typography } from 'src/components/Typography'; import { DiskSelect } from 'src/features/Linodes/DiskSelect/DiskSelect'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useFlags } from 'src/hooks/useFlags'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useCreateImageMutation } from 'src/queries/images'; import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -102,7 +102,7 @@ export const CreateImageTab: React.FC = (props) => { const { mutateAsync: createImage } = useCreateImageMutation(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const [selectedLinode, setSelectedLinode] = React.useState(); const [selectedDisk, setSelectedDisk] = React.useState(''); diff --git a/packages/manager/src/features/Images/ImagesDrawer.tsx b/packages/manager/src/features/Images/ImagesDrawer.tsx index ff4bb9234f8..1b13dabc697 100644 --- a/packages/manager/src/features/Images/ImagesDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesDrawer.tsx @@ -1,11 +1,11 @@ import { Disk, getLinodeDisks } from '@linode/api-v4/lib/linodes'; import { APIError } from '@linode/api-v4/lib/types'; import { Theme } from '@mui/material/styles'; -import { makeStyles } from 'tss-react/mui'; import { useSnackbar } from 'notistack'; import { equals } from 'ramda'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; +import { makeStyles } from 'tss-react/mui'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; @@ -15,6 +15,7 @@ import { Typography } from 'src/components/Typography'; import { IMAGE_DEFAULT_LIMIT } from 'src/constants'; import { DiskSelect } from 'src/features/Linodes/DiskSelect/DiskSelect'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useCreateImageMutation, useUpdateImageMutation, @@ -23,7 +24,6 @@ import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { useImageAndLinodeGrantCheck } from './utils'; -import { usePollingInterval } from 'src/queries/events/events'; const useStyles = makeStyles()((theme: Theme) => ({ actionPanel: { @@ -102,7 +102,7 @@ export const ImagesDrawer = (props: CombinedProps) => { permissionedLinodes: availableLinodes, } = useImageAndLinodeGrantCheck(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const [mounted, setMounted] = React.useState(false); const [notice, setNotice] = React.useState(undefined); diff --git a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx index d6dc56ce920..04674267d4a 100644 --- a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx +++ b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx @@ -27,7 +27,7 @@ import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { @@ -59,7 +59,7 @@ const CloneLanding = () => { const location = useLocation(); const theme = useTheme(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const linodeId = Number(_linodeId); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx index 4c0e9c9075f..e6d5d7ce02c 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -21,6 +21,10 @@ import { WithAccountSettingsProps, withAccountSettings, } from 'src/containers/accountSettings.container'; +import { + WithEventsPollingActionProps, + withEventsPollingActions, +} from 'src/containers/events.container'; import withImages, { DefaultProps as ImagesProps, } from 'src/containers/images.container'; @@ -62,11 +66,11 @@ import { } from 'src/utilities/analytics'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { ExtendedType, extendType } from 'src/utilities/extendType'; +import { isEURegion } from 'src/utilities/formatRegion'; import { getGDPRDetails, getSelectedRegionGroup, } from 'src/utilities/formatRegion'; -import { isEURegion } from 'src/utilities/formatRegion'; import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; import { getQueryParamsFromQueryString } from 'src/utilities/queryParams'; @@ -85,7 +89,6 @@ import type { LinodeTypeClass, PriceObject, } from '@linode/api-v4/lib/linodes'; -import { WithPollingIntervalProps, withPollingInterval } from 'src/containers/events.container'; const DEFAULT_IMAGE = 'linode/debian11'; @@ -138,7 +141,7 @@ type CombinedProps = WithSnackbarProps & WithQueryClientProps & WithMarketplaceAppsProps & WithAccountSettingsProps & - WithPollingIntervalProps; + WithEventsPollingActionProps; const defaultState: State = { assignPublicIPv4Address: false, @@ -947,7 +950,7 @@ export default recompose( withQueryClient, withAccountSettings, withMarketplaceApps, - withPollingInterval + withEventsPollingActions )(LinodeCreateContainer); const actionsAndLabels = { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx index 128f1aa3239..f91c5895b64 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useLinodeBackupsCancelMutation } from 'src/queries/linodes/backups'; import { sendBackupsDisabledEvent } from 'src/utilities/analytics'; @@ -24,7 +24,7 @@ export const CancelBackupsDialog = (props: Props) => { mutateAsync: cancelBackups, } = useLinodeBackupsCancelMutation(linodeId); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const onCancelBackups = async () => { await cancelBackups(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx index 25aee468f7d..aaf7c88b2c1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx @@ -10,7 +10,7 @@ import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useLinodeBackupSnapshotMutation } from 'src/queries/linodes/backups'; import { getErrorMap } from 'src/utilities/errorUtils'; @@ -25,7 +25,7 @@ export const CaptureSnapshot = (props: Props) => { const { isReadOnly, linodeId } = props; const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { error: snapshotError, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx index 456c25f7e08..a004daaa5ba 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx @@ -7,7 +7,7 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { Currency } from 'src/components/Currency'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useLinodeBackupsEnableMutation } from 'src/queries/linodes/backups'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useTypeQuery } from 'src/queries/types'; @@ -49,7 +49,7 @@ export const EnableBackupsDialog = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const handleEnableBackups = async () => { await enableBackups(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx index 599cea29bf6..a9f81aa3538 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx @@ -11,7 +11,7 @@ import { FormControl } from 'src/components/FormControl'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { FormHelperText } from 'src/components/FormHelperText'; import { Notice } from 'src/components/Notice/Notice'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useLinodeBackupRestoreMutation } from 'src/queries/linodes/backups'; import { useAllLinodesQuery, @@ -31,7 +31,7 @@ export const RestoreToLinodeDrawer = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); const { data: linode } = useLinodeQuery(linodeId, open); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { data: linodes, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx index 85e7f2db957..1ef6b84473d 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useRebootLinodeMutation } from 'src/queries/linodes/linodes'; interface Props { @@ -21,7 +21,7 @@ export const BootConfigDialog = (props: Props) => { const { error, isLoading, mutateAsync } = useRebootLinodeMutation(linodeId); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const onBoot = async () => { await mutateAsync({ config_id: config?.id ?? -1 }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx index a3524e2f61e..1bc799c30da 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx @@ -19,6 +19,7 @@ import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; import { UserDataAccordion } from 'src/features/Linodes/LinodesCreate/UserDataAccordion/UserDataAccordion'; import { regionSupportsMetadata } from 'src/features/Linodes/LinodesCreate/utilities'; import { useFlags } from 'src/hooks/useFlags'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllImagesQuery } from 'src/queries/images'; import { usePreferences } from 'src/queries/preferences'; import { useRegionsQuery } from 'src/queries/regions'; @@ -35,7 +36,6 @@ import { StyledGrid, StyledNotice, } from './RebuildFromImage.styles'; -import { usePollingInterval } from 'src/queries/events/events'; interface Props { disabled: boolean; @@ -79,7 +79,7 @@ export const RebuildFromImage = (props: Props) => { isLoading: isLoadingPreferences, } = usePreferences(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { enqueueSnackbar } = useSnackbar(); const flags = useFlags(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx index af84981755f..8fb913d76f5 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx @@ -23,7 +23,7 @@ import { } from 'src/features/StackScripts/stackScriptUtils'; import { useStackScript } from 'src/hooks/useStackScript'; import { listToItemsByID } from 'src/queries/base'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllImagesQuery } from 'src/queries/images'; import { usePreferences } from 'src/queries/preferences'; import { filterImagesByType } from 'src/store/image/image.helpers'; @@ -73,7 +73,7 @@ export const RebuildFromStackScript = (props: Props) => { isLoading: isLoadingPreferences, } = usePreferences(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx index 3ce190acb08..a243af1f434 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -26,7 +26,7 @@ export const BareMetalRescue = (props: Props) => { linodeId !== undefined && isOpen ); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); React.useEffect(() => { if (isOpen) { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx index 930df106956..18b49c3069f 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx @@ -11,7 +11,7 @@ import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { usePrevious } from 'src/hooks/usePrevious'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeQuery, @@ -142,7 +142,7 @@ export const StandardRescueDialog = (props: Props) => { deviceMap ); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { enqueueSnackbar } = useSnackbar(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx index 16d823b3424..1ef9611af95 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx @@ -19,7 +19,7 @@ import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; import { Typography } from 'src/components/Typography'; import { linodeInTransition } from 'src/features/Linodes/transitions'; import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeQuery, @@ -85,7 +85,7 @@ export const LinodeResize = (props: Props) => { mutateAsync: resizeLinode, } = useLinodeResizeMutation(linodeId ?? -1); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { data: regionsData } = useRegionsQuery(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx index 9a5a4ee5d3e..c0a1f547a1f 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx @@ -6,7 +6,7 @@ import { Button } from 'src/components/Button/Button'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useDeleteLinodeMutation, useLinodeQuery, @@ -26,7 +26,7 @@ export const LinodeSettingsDeletePanel = (props: Props) => { mutateAsync: deleteLinode, } = useDeleteLinodeMutation(linodeId); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const history = useHistory(); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx index 6d3fba970eb..b3841f39161 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx @@ -16,6 +16,7 @@ import { InputAdornment } from 'src/components/InputAdornment'; import { Mode, ModeSelect } from 'src/components/ModeSelect/ModeSelect'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllLinodeDisksQuery, useLinodeDiskCreateMutation, @@ -24,7 +25,6 @@ import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import { ImageAndPassword } from '../LinodeSettings/ImageAndPassword'; -import { usePollingInterval } from 'src/queries/events/events'; type FileSystem = 'ext3' | 'ext4' | 'initrd' | 'raw' | 'swap'; @@ -51,7 +51,7 @@ export const CreateDiskDrawer = (props: Props) => { const { linodeId, onClose, open } = props; const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const [selectedMode, setSelectedMode] = React.useState('empty'); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx index c8f5ca49d22..97fb6c451b1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx @@ -14,7 +14,7 @@ import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { TextTooltip } from 'src/components/TextTooltip'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllLinodeDisksQuery, useLinodeDiskResizeMutation, @@ -45,7 +45,7 @@ export const ResizeDiskDrawer = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { mutateAsync: resizeDisk, reset } = useLinodeDiskResizeMutation( linodeId, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx index d6710b787f9..bdaa712569d 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; import { MBpsIntraDC } from 'src/constants'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useStartLinodeMutationMutation } from 'src/queries/linodes/actions'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; @@ -24,7 +24,7 @@ export const MutationNotification = (props: Props) => { const { data: linode } = useLinodeQuery(linodeId); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { data: currentTypeInfo } = useTypeQuery( linode?.type ?? '', diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx index 5b320764309..a25d79f10f2 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx @@ -6,7 +6,7 @@ import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToCo import { Typography } from 'src/components/Typography'; import { useFlags } from 'src/hooks/useFlags'; import { useAccount } from 'src/queries/account'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useDeleteLinodeMutation, @@ -29,7 +29,7 @@ export const DeleteLinodeDialog = (props: Props) => { const flags = useFlags(); const { data: account } = useAccount(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const enableVPCActions = isFeatureEnabled( 'VPCs', diff --git a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx index 095ca324c88..c47f227a78a 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx @@ -18,9 +18,10 @@ import { useAccountAgreements, useMutateAccountAgreements, } from 'src/queries/accountAgreements'; +import { isEventRelevantToLinode } from 'src/queries/events/event.helpers'; import { + useEventsPollingActions, useInProgressEvents, - usePollingInterval, } from 'src/queries/events/events'; import { useImageQuery } from 'src/queries/images'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; @@ -31,7 +32,6 @@ import { import { useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { useTypeQuery } from 'src/queries/types'; -import { isEventRelevantToLinode } from 'src/queries/events/event.helpers'; import { sendMigrationInitiatedEvent } from 'src/utilities/analytics'; import { formatDate } from 'src/utilities/formatDate'; import { getGDPRDetails } from 'src/utilities/formatRegion'; @@ -54,7 +54,7 @@ export const MigrateLinode = React.memo((props: Props) => { const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { data: linode } = useLinodeQuery( linodeId ?? -1, diff --git a/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx b/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx index 9195c28721c..2dbf68d2018 100644 --- a/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx +++ b/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx @@ -8,7 +8,7 @@ import Select from 'src/components/EnhancedSelect/Select'; import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useBootLinodeMutation, @@ -80,7 +80,7 @@ export const PowerActionsDialog = (props: Props) => { mutateAsync: shutdownLinode, } = useShutdownLinodeMutation(linodeId ?? -1); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const [selectedConfigID, setSelectConfigID] = React.useState( null diff --git a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx index 4fc0ed854fd..a6cdd657976 100644 --- a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx @@ -11,11 +11,11 @@ import { FormControl } from 'src/components/FormControl'; import { FormHelperText } from 'src/components/FormHelperText'; import { Notice } from 'src/components/Notice/Notice'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useGrants } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; -import { usePollingInterval } from 'src/queries/events/events'; interface Props { onClose: () => void; @@ -37,7 +37,7 @@ export const AttachVolumeDrawer = React.memo((props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { data: grants } = useGrants(); diff --git a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx b/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx index 6f51aae19bf..2209cba5691 100644 --- a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx @@ -8,7 +8,7 @@ import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useGrants } from 'src/queries/profile'; import { useCloneVolumeMutation } from 'src/queries/volumes'; import { @@ -31,7 +31,7 @@ export const CloneVolumeDrawer = (props: Props) => { const { mutateAsync: cloneVolume } = useCloneVolumeMutation(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { data: grants } = useGrants(); diff --git a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx b/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx index 0b3da1373c7..8ddde52041b 100644 --- a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useDeleteVolumeMutation } from 'src/queries/volumes'; interface Props { @@ -21,7 +21,7 @@ export const DeleteVolumeDialog = (props: Props) => { mutateAsync: deleteVolume, } = useDeleteVolumeMutation(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const onDelete = () => { deleteVolume({ id: volume?.id ?? -1 }).then(() => { diff --git a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx index d64f65f9b5d..cb944f90e13 100644 --- a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { Typography } from 'src/components/Typography'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useDetachVolumeMutation } from 'src/queries/volumes'; @@ -20,7 +20,7 @@ export const DetachVolumeDialog = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const { data: linode } = useLinodeQuery( volume?.linode_id ?? -1, diff --git a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx b/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx index 6f6d2a402fe..56cc101b3fd 100644 --- a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useGrants } from 'src/queries/profile'; import { useResizeVolumeMutation } from 'src/queries/volumes'; import { @@ -16,7 +17,6 @@ import { import { PricePanel } from './VolumeDrawer/PricePanel'; import { SizeField } from './VolumeDrawer/SizeField'; -import { usePollingInterval } from 'src/queries/events/events'; interface Props { onClose: () => void; @@ -29,7 +29,7 @@ export const ResizeVolumeDrawer = (props: Props) => { const { mutateAsync: resizeVolume } = useResizeVolumeMutation(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const validationSchema = ResizeVolumeSchema(volume?.size ?? -1); diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx index 5382e37a516..b477906b011 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx @@ -7,7 +7,7 @@ import { number, object } from 'yup'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Notice } from 'src/components/Notice/Notice'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useGrants } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; import { @@ -46,7 +46,7 @@ export const LinodeVolumeAttachForm = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const linodeGrant = grants?.linode.find( (grant: Grant) => grant.id === linode.id diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx index 1813041e8cf..473ec6eea94 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx @@ -10,7 +10,7 @@ import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; import { MAX_VOLUME_SIZE } from 'src/constants'; -import { usePollingInterval } from 'src/queries/events/events'; +import { useEventsPollingActions } from 'src/queries/events/events'; import { useGrants, useProfile } from 'src/queries/profile'; import { useCreateVolumeMutation } from 'src/queries/volumes'; import { sendCreateVolumeEvent } from 'src/utilities/analytics'; @@ -57,7 +57,7 @@ export const LinodeVolumeCreateForm = (props: Props) => { const { data: grants } = useGrants(); const { mutateAsync: createVolume } = useCreateVolumeMutation(); - const { resetEventsPolling } = usePollingInterval(); + const { resetEventsPolling } = useEventsPollingActions(); const disabled = profile?.restricted && !grants?.global.add_volumes; diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index cd90a9f36e3..35a38346f57 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -183,18 +183,17 @@ export const usePollingInterval = () => { }; /** - * Manages the events polling interval. + * This hook manages the events polling interval. * * This hook should be used in application components that need to change - * the events polling interval. + * the events polling interval. It performs actions, but does not return any state + * in hopes to prevent extra rendering. */ export const useEventsPollingActions = () => { const queryClient = useQueryClient(); - const resetEventsPolling = queryClient.setQueryData( - pollingIntervalQueryKey, - 1 - ); + const resetEventsPolling = () => + queryClient.setQueryData(pollingIntervalQueryKey, 1); return { /** From 8d7436e5e355d6ef2a280d46975706690d53ac55 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 19 Jan 2024 13:50:16 -0500 Subject: [PATCH 36/44] simplify polling interval --- packages/manager/src/queries/events/events.ts | 47 +++---------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 35a38346f57..21f2f4160e4 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -11,11 +11,7 @@ import { useQueryClient, } from 'react-query'; -import { - DISABLE_EVENT_THROTTLE, - INTERVAL, - ISO_DATETIME_NO_TZ_FORMAT, -} from 'src/constants'; +import { ISO_DATETIME_NO_TZ_FORMAT } from 'src/constants'; import { useEventHandlers } from 'src/hooks/useEventHandlers'; import { useToastNotifications } from 'src/hooks/useToastNotifications'; import { @@ -98,8 +94,6 @@ export const useInProgressEvents = () => { * *Warning* This hook should only be mounted once! */ export const useEventsPoller = () => { - const { incrementPollingInterval, pollingInterval } = usePollingInterval(); - const { handleGlobalToast } = useToastNotifications(); const { handleEvent } = useEventHandlers(); @@ -134,8 +128,6 @@ export const useEventsPoller = () => { useQuery({ enabled: hasFetchedInitialEvents, onSuccess(events) { - incrementPollingInterval(); - if (events.length > 0) { updateEventsQueries(events, queryClient); @@ -147,41 +139,16 @@ export const useEventsPoller = () => { }, queryFn: () => getEvents({}, filter).then((data) => data.data), queryKey: ['events', 'poller', hasFetchedInitialEvents], - refetchInterval: pollingInterval, + // The /v4/account/events endpoint has a rate-limit of 400 requets per minute. + // If we request events every 5 seconds, we will make 12 calls in 1 minute. + // If we request events every 2.5 seconds, we will make 24 calls in 1 minute. + // If we request events every 1 second, we will make 60 calls in 1 minute. + refetchInterval: inProgressEvents.length > 0 ? 2_500 : 16_000, }); return null; }; -const pollingIntervalQueryKey = ['events', 'interval']; - -/** - * Manages and exposes the events polling interval. - */ -export const usePollingInterval = () => { - const queryClient = useQueryClient(); - const { data: intervalMultiplier = 1 } = useQuery({ - enabled: false, - initialData: 1, - queryKey: pollingIntervalQueryKey, - }); - return { - /** - * Increases the polling interval by 1 second up to 16 seconds - */ - incrementPollingInterval: () => - queryClient.setQueryData( - pollingIntervalQueryKey, - Math.min(intervalMultiplier * 2, 16) - ), - pollingInterval: DISABLE_EVENT_THROTTLE - ? 500 - : intervalMultiplier * INTERVAL, - resetEventsPolling: () => - queryClient.setQueryData(pollingIntervalQueryKey, 1), - }; -}; - /** * This hook manages the events polling interval. * @@ -193,7 +160,7 @@ export const useEventsPollingActions = () => { const queryClient = useQueryClient(); const resetEventsPolling = () => - queryClient.setQueryData(pollingIntervalQueryKey, 1); + queryClient.invalidateQueries(['events', 'poller']); return { /** From 3e99b0fc4834f5e58cf77ed9a108e64116610913 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 22 Jan 2024 10:15:17 -0500 Subject: [PATCH 37/44] fixing duplicate pull due to rules of hooks --- packages/manager/src/queries/events/events.ts | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 21f2f4160e4..0dac9418c06 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -18,6 +18,7 @@ import { doesEventMatchAPIFilter, generatePollingFilter, getExistingEventDataForPollingFilterGenerator, + isInProgressEvent, } from 'src/queries/events/event.helpers'; import type { APIError, Event, Filter, ResourcePage } from '@linode/api-v4'; @@ -101,30 +102,14 @@ export const useEventsPoller = () => { const { events } = useEventsInfiniteQuery(); + const hasFetchedInitialEvents = events !== undefined; + const [mountTimestamp] = useState(() => DateTime.fromMillis(Date.now(), { zone: 'utc' }).toFormat( ISO_DATETIME_NO_TZ_FORMAT ) ); - // 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. - const latestEventTime = - events && events.length > 0 ? events[0].created : mountTimestamp; - - const { - eventsThatAlreadyHappenedAtTheFilterTime, - inProgressEvents, - } = getExistingEventDataForPollingFilterGenerator(events, latestEventTime); - - const hasFetchedInitialEvents = events !== undefined; - - const filter = generatePollingFilter( - latestEventTime, - inProgressEvents, - eventsThatAlreadyHappenedAtTheFilterTime - ); - useQuery({ enabled: hasFetchedInitialEvents, onSuccess(events) { @@ -137,13 +122,49 @@ export const useEventsPoller = () => { } } }, - queryFn: () => getEvents({}, filter).then((data) => data.data), + queryFn: () => { + const data = queryClient.getQueryData>>([ + 'events', + 'infinite', + undefined, + ]); + 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. + const latestEventTime = + events && events.length > 0 ? events[0].created : mountTimestamp; + + const { + eventsThatAlreadyHappenedAtTheFilterTime, + inProgressEvents, + } = getExistingEventDataForPollingFilterGenerator( + events, + latestEventTime + ); + + const filter = generatePollingFilter( + latestEventTime, + inProgressEvents, + eventsThatAlreadyHappenedAtTheFilterTime + ); + + return getEvents({}, filter).then((data) => data.data); + }, queryKey: ['events', 'poller', hasFetchedInitialEvents], // The /v4/account/events endpoint has a rate-limit of 400 requets per minute. // If we request events every 5 seconds, we will make 12 calls in 1 minute. // If we request events every 2.5 seconds, we will make 24 calls in 1 minute. // If we request events every 1 second, we will make 60 calls in 1 minute. - refetchInterval: inProgressEvents.length > 0 ? 2_500 : 16_000, + refetchInterval: (data) => { + const hasEventsStillInProgress = data?.some(isInProgressEvent); + if (hasEventsStillInProgress) { + return 5_000; + } + return 16_000; + }, }); return null; @@ -160,7 +181,7 @@ export const useEventsPollingActions = () => { const queryClient = useQueryClient(); const resetEventsPolling = () => - queryClient.invalidateQueries(['events', 'poller']); + queryClient.invalidateQueries(['events', 'poller', true]); return { /** From c0e0c345c829a012d356299f3d90a59361a3c3a7 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 22 Jan 2024 16:03:24 -0500 Subject: [PATCH 38/44] clean up new polling interval --- packages/manager/src/constants.ts | 20 ++++++++++++++++--- packages/manager/src/queries/events/events.ts | 14 +++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/manager/src/constants.ts b/packages/manager/src/constants.ts index c20c1c18442..23fc3560cab 100644 --- a/packages/manager/src/constants.ts +++ b/packages/manager/src/constants.ts @@ -100,10 +100,24 @@ export const MAX_VOLUME_SIZE = 10240; export const MAX_MONTHS_EOL_FILTER = 6; /** - * The lowest interval at which to make an Events request. This is later multiplied by the pollIteration - * to get the actual interval. + * Values used for our events polling system. + * Number values are in milliseconds */ -export const INTERVAL = 1000; +export const POLLING_INTERVALS = { + /** + * By default, we will poll for events every 16 seconds + */ + DEFAULT: 16_000, + /** + * If there are "in-progress" events, we will poll every 2 seconds to give users + * a real-time feeling experience + * + * The /v4/account/events endpoint has a rate-limit of 400 requets per minute. + * If we request events every 2 seconds, we will make 30 calls in 1 minute. + * We should be well within rate-limits. + */ + IN_PROGRESS: 2_000, +} as const; /** * Time after which data from the API is considered stale (half an hour) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 0dac9418c06..683db0fa8e1 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -11,7 +11,7 @@ import { useQueryClient, } from 'react-query'; -import { ISO_DATETIME_NO_TZ_FORMAT } from 'src/constants'; +import { ISO_DATETIME_NO_TZ_FORMAT, POLLING_INTERVALS } from 'src/constants'; import { useEventHandlers } from 'src/hooks/useEventHandlers'; import { useToastNotifications } from 'src/hooks/useToastNotifications'; import { @@ -154,16 +154,12 @@ export const useEventsPoller = () => { return getEvents({}, filter).then((data) => data.data); }, queryKey: ['events', 'poller', hasFetchedInitialEvents], - // The /v4/account/events endpoint has a rate-limit of 400 requets per minute. - // If we request events every 5 seconds, we will make 12 calls in 1 minute. - // If we request events every 2.5 seconds, we will make 24 calls in 1 minute. - // If we request events every 1 second, we will make 60 calls in 1 minute. refetchInterval: (data) => { - const hasEventsStillInProgress = data?.some(isInProgressEvent); - if (hasEventsStillInProgress) { - return 5_000; + const hasInProgressEvents = data?.some(isInProgressEvent); + if (hasInProgressEvents) { + return POLLING_INTERVALS.IN_PROGRESS; } - return 16_000; + return POLLING_INTERVALS.DEFAULT; }, }); From 527ab37a097798a37870f56e5e1836688c161a10 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 23 Jan 2024 08:53:47 -0500 Subject: [PATCH 39/44] add changesets --- packages/api-v4/.changeset/pr-9949-fixed-1706018014628.md | 5 +++++ .../manager/.changeset/pr-9949-tech-stories-1706017963330.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-9949-fixed-1706018014628.md create mode 100644 packages/manager/.changeset/pr-9949-tech-stories-1706017963330.md diff --git a/packages/api-v4/.changeset/pr-9949-fixed-1706018014628.md b/packages/api-v4/.changeset/pr-9949-fixed-1706018014628.md new file mode 100644 index 00000000000..119590213ce --- /dev/null +++ b/packages/api-v4/.changeset/pr-9949-fixed-1706018014628.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Fixed +--- + +Removed incorrect `_initial` property on `Event` type ([#9949](https://github.com/linode/manager/pull/9949)) diff --git a/packages/manager/.changeset/pr-9949-tech-stories-1706017963330.md b/packages/manager/.changeset/pr-9949-tech-stories-1706017963330.md new file mode 100644 index 00000000000..52c2d308dd4 --- /dev/null +++ b/packages/manager/.changeset/pr-9949-tech-stories-1706017963330.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +React Query for Events ([#9949](https://github.com/linode/manager/pull/9949)) From 7a9fdbf009cf8985af2c82c2dbb18bb45745845c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 23 Jan 2024 10:10:20 -0500 Subject: [PATCH 40/44] clean up mount timestamp --- packages/manager/src/queries/events/events.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 683db0fa8e1..0184b936167 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -1,6 +1,6 @@ import { getEvents, markEventSeen } from '@linode/api-v4'; import { DateTime } from 'luxon'; -import { useState } from 'react'; +import { useRef } from 'react'; import { InfiniteData, QueryClient, @@ -104,10 +104,8 @@ export const useEventsPoller = () => { const hasFetchedInitialEvents = events !== undefined; - const [mountTimestamp] = useState(() => - DateTime.fromMillis(Date.now(), { zone: 'utc' }).toFormat( - ISO_DATETIME_NO_TZ_FORMAT - ) + const mountTimestamp = useRef( + DateTime.now().setZone('utc').toFormat(ISO_DATETIME_NO_TZ_FORMAT) ); useQuery({ @@ -135,7 +133,9 @@ export const useEventsPoller = () => { // 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. const latestEventTime = - events && events.length > 0 ? events[0].created : mountTimestamp; + events && events.length > 0 + ? events[0].created + : mountTimestamp.current; const { eventsThatAlreadyHappenedAtTheFilterTime, From 9086b936a9663b949dec79e9f9f5c96522607f11 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 23 Jan 2024 10:19:26 -0500 Subject: [PATCH 41/44] clean up query key for poller --- packages/manager/src/queries/events/events.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index 0184b936167..b0d0122806f 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -84,7 +84,7 @@ export const useEventsInfiniteQuery = (filter?: Filter) => { export const useInProgressEvents = () => { return useQuery({ enabled: false, - queryKey: ['events', 'poller', true], + queryKey: ['events', 'poller'], }); }; @@ -153,7 +153,7 @@ export const useEventsPoller = () => { return getEvents({}, filter).then((data) => data.data); }, - queryKey: ['events', 'poller', hasFetchedInitialEvents], + queryKey: ['events', 'poller'], refetchInterval: (data) => { const hasInProgressEvents = data?.some(isInProgressEvent); if (hasInProgressEvents) { @@ -167,17 +167,14 @@ export const useEventsPoller = () => { }; /** - * This hook manages the events polling interval. - * - * This hook should be used in application components that need to change - * the events polling interval. It performs actions, but does not return any state - * in hopes to prevent extra rendering. + * This hook returns functions that allow us to interact + * with our events polling system. */ export const useEventsPollingActions = () => { const queryClient = useQueryClient(); const resetEventsPolling = () => - queryClient.invalidateQueries(['events', 'poller', true]); + queryClient.invalidateQueries(['events', 'poller']); return { /** From 126f7b8831aaab16abf1557f132ab0171b15a33d Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 23 Jan 2024 10:31:44 -0500 Subject: [PATCH 42/44] rename `resetEventsPolling` to `checkForNewEvents` --- .../Images/ImagesCreate/CreateImageTab.tsx | 4 ++-- .../manager/src/features/Images/ImagesDrawer.tsx | 4 ++-- .../features/Linodes/CloneLanding/CloneLanding.tsx | 4 ++-- .../LinodesCreate/LinodeCreateContainer.tsx | 2 +- .../LinodeBackup/CancelBackupsDialog.tsx | 4 ++-- .../LinodesDetail/LinodeBackup/CaptureSnapshot.tsx | 4 ++-- .../LinodeBackup/EnableBackupsDialog.tsx | 4 ++-- .../LinodeBackup/RestoreToLinodeDrawer.tsx | 4 ++-- .../LinodeConfigs/BootConfigDialog.tsx | 4 ++-- .../LinodeRebuild/RebuildFromImage.tsx | 4 ++-- .../LinodeRebuild/RebuildFromStackScript.tsx | 4 ++-- .../LinodesDetail/LinodeRescue/BareMetalRescue.tsx | 4 ++-- .../LinodeRescue/StandardRescueDialog.tsx | 4 ++-- .../LinodesDetail/LinodeResize/LinodeResize.tsx | 4 ++-- .../LinodeSettings/LinodeSettingsDeletePanel.tsx | 4 ++-- .../LinodeStorage/CreateDiskDrawer.tsx | 4 ++-- .../LinodeStorage/ResizeDiskDrawer.tsx | 4 ++-- .../LinodesDetailHeader/MutationNotification.tsx | 4 ++-- .../Linodes/LinodesLanding/DeleteLinodeDialog.tsx | 4 ++-- .../Linodes/MigrateLinode/MigrateLinode.tsx | 4 ++-- .../Linodes/PowerActionsDialogOrDrawer.tsx | 4 ++-- .../src/features/Volumes/AttachVolumeDrawer.tsx | 4 ++-- .../src/features/Volumes/CloneVolumeDrawer.tsx | 4 ++-- .../src/features/Volumes/DeleteVolumeDialog.tsx | 4 ++-- .../src/features/Volumes/DetachVolumeDialog.tsx | 4 ++-- .../src/features/Volumes/ResizeVolumeDrawer.tsx | 4 ++-- .../VolumeDrawer/LinodeVolumeAttachForm.tsx | 4 ++-- .../VolumeDrawer/LinodeVolumeCreateForm.tsx | 4 ++-- packages/manager/src/queries/events/events.ts | 14 ++++++++++---- 29 files changed, 65 insertions(+), 59 deletions(-) diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index c72e40153a9..6773a912703 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -102,7 +102,7 @@ export const CreateImageTab: React.FC = (props) => { const { mutateAsync: createImage } = useCreateImageMutation(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const [selectedLinode, setSelectedLinode] = React.useState(); const [selectedDisk, setSelectedDisk] = React.useState(''); @@ -173,7 +173,7 @@ export const CreateImageTab: React.FC = (props) => { label, }) .then((_) => { - resetEventsPolling(); + checkForNewEvents(); setSubmitting(false); diff --git a/packages/manager/src/features/Images/ImagesDrawer.tsx b/packages/manager/src/features/Images/ImagesDrawer.tsx index 1b13dabc697..bbe5b17e6b3 100644 --- a/packages/manager/src/features/Images/ImagesDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesDrawer.tsx @@ -102,7 +102,7 @@ export const ImagesDrawer = (props: CombinedProps) => { permissionedLinodes: availableLinodes, } = useImageAndLinodeGrantCheck(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const [mounted, setMounted] = React.useState(false); const [notice, setNotice] = React.useState(undefined); @@ -230,7 +230,7 @@ export const ImagesDrawer = (props: CombinedProps) => { return; } - resetEventsPolling(); + checkForNewEvents(); setSubmitting(false); diff --git a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx index 04674267d4a..148c7c40aad 100644 --- a/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx +++ b/packages/manager/src/features/Linodes/CloneLanding/CloneLanding.tsx @@ -59,7 +59,7 @@ const CloneLanding = () => { const location = useLocation(); const theme = useTheme(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const linodeId = Number(_linodeId); @@ -242,7 +242,7 @@ const CloneLanding = () => { request() .then(() => { setSubmitting(false); - resetEventsPolling(); + checkForNewEvents(); history.push(`/linodes/${linodeId}/configurations`); }) .catch((errors) => { diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx index e6d5d7ce02c..72ea5e286bb 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -870,7 +870,7 @@ class LinodeCreateContainer extends React.PureComponent { ); /** reset the Events polling */ - this.props.resetEventsPolling(); + this.props.checkForNewEvents(); // If a VPC was assigned, invalidate the query so that the relevant VPC data // gets displayed in the LinodeEntityDetail diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx index f91c5895b64..51b52d4db5e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CancelBackupsDialog.tsx @@ -24,7 +24,7 @@ export const CancelBackupsDialog = (props: Props) => { mutateAsync: cancelBackups, } = useLinodeBackupsCancelMutation(linodeId); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const onCancelBackups = async () => { await cancelBackups(); @@ -32,7 +32,7 @@ export const CancelBackupsDialog = (props: Props) => { variant: 'info', }); onClose(); - resetEventsPolling(); + checkForNewEvents(); sendBackupsDisabledEvent(); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx index aaf7c88b2c1..3790cbfa58c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.tsx @@ -25,7 +25,7 @@ export const CaptureSnapshot = (props: Props) => { const { isReadOnly, linodeId } = props; const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { error: snapshotError, @@ -47,7 +47,7 @@ export const CaptureSnapshot = (props: Props) => { }); setIsSnapshotConfirmationDialogOpen(false); formikHelpers.resetForm(); - resetEventsPolling(); + checkForNewEvents(); }, }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx index a004daaa5ba..327aa57580e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx @@ -49,11 +49,11 @@ export const EnableBackupsDialog = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const handleEnableBackups = async () => { await enableBackups(); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar('Backups are being enabled for this Linode.', { variant: 'success', }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx index a9f81aa3538..f5800389e6c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.tsx @@ -31,7 +31,7 @@ export const RestoreToLinodeDrawer = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); const { data: linode } = useLinodeQuery(linodeId, open); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { data: linodes, @@ -69,7 +69,7 @@ export const RestoreToLinodeDrawer = (props: Props) => { `Started restoring Linode ${selectedLinodeOption?.label} from a backup`, { variant: 'info' } ); - resetEventsPolling(); + checkForNewEvents(); onClose(); }, }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx index 1ef6b84473d..59562064701 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/BootConfigDialog.tsx @@ -21,11 +21,11 @@ export const BootConfigDialog = (props: Props) => { const { error, isLoading, mutateAsync } = useRebootLinodeMutation(linodeId); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const onBoot = async () => { await mutateAsync({ config_id: config?.id ?? -1 }); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar(`Successfully booted config ${config?.label}`, { variant: 'success', }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx index 1bc799c30da..9ce3b556f4f 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx @@ -79,7 +79,7 @@ export const RebuildFromImage = (props: Props) => { isLoading: isLoadingPreferences, } = usePreferences(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { enqueueSnackbar } = useSnackbar(); const flags = useFlags(); @@ -155,7 +155,7 @@ export const RebuildFromImage = (props: Props) => { rebuildLinode(linodeId, params) .then((_) => { // Reset events polling since an in-progress event (rebuild) is happening. - resetEventsPolling(); + checkForNewEvents(); setSubmitting(false); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx index 8fb913d76f5..cf4e0f2ba77 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx @@ -73,7 +73,7 @@ export const RebuildFromStackScript = (props: Props) => { isLoading: isLoadingPreferences, } = usePreferences(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); @@ -129,7 +129,7 @@ export const RebuildFromStackScript = (props: Props) => { }) .then((_) => { // Reset events polling since an in-progress event (rebuild) is happening. - resetEventsPolling(); + checkForNewEvents(); setSubmitting(false); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx index a243af1f434..d8d0d5c1030 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/BareMetalRescue.tsx @@ -26,7 +26,7 @@ export const BareMetalRescue = (props: Props) => { linodeId !== undefined && isOpen ); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); React.useEffect(() => { if (isOpen) { @@ -44,7 +44,7 @@ export const BareMetalRescue = (props: Props) => { enqueueSnackbar('Linode rescue started.', { variant: 'info', }); - resetEventsPolling(); + checkForNewEvents(); onClose(); }) .catch((err) => { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx index 18b49c3069f..83ccb886572 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx @@ -142,7 +142,7 @@ export const StandardRescueDialog = (props: Props) => { deviceMap ); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { enqueueSnackbar } = useSnackbar(); @@ -173,7 +173,7 @@ export const StandardRescueDialog = (props: Props) => { enqueueSnackbar('Linode rescue started.', { variant: 'info', }); - resetEventsPolling(); + checkForNewEvents(); onClose(); }) .catch((errorResponse: APIError[]) => { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx index 1ef9611af95..d63b0cde6e4 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx @@ -85,7 +85,7 @@ export const LinodeResize = (props: Props) => { mutateAsync: resizeLinode, } = useLinodeResizeMutation(linodeId ?? -1); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { data: regionsData } = useRegionsQuery(); @@ -117,7 +117,7 @@ export const LinodeResize = (props: Props) => { migration_type: values.migration_type, type: values.type, }); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar('Linode queued for resize.', { variant: 'info', }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx index c0a1f547a1f..ff27b1d8c9b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsDeletePanel.tsx @@ -26,7 +26,7 @@ export const LinodeSettingsDeletePanel = (props: Props) => { mutateAsync: deleteLinode, } = useDeleteLinodeMutation(linodeId); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const history = useHistory(); @@ -34,7 +34,7 @@ export const LinodeSettingsDeletePanel = (props: Props) => { const onDelete = async () => { await deleteLinode(); - resetEventsPolling(); + checkForNewEvents(); history.push('/linodes'); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx index b3841f39161..30fe4d0df74 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx @@ -51,7 +51,7 @@ export const CreateDiskDrawer = (props: Props) => { const { linodeId, onClose, open } = props; const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const [selectedMode, setSelectedMode] = React.useState('empty'); @@ -94,7 +94,7 @@ export const CreateDiskDrawer = (props: Props) => { : values; await createDisk(cleanedValues); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar(`Started creation of disk ${values.label}`, { variant: 'success', }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx index 97fb6c451b1..1218f35c4a6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.tsx @@ -45,7 +45,7 @@ export const ResizeDiskDrawer = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { mutateAsync: resizeDisk, reset } = useLinodeDiskResizeMutation( linodeId, @@ -66,7 +66,7 @@ export const ResizeDiskDrawer = (props: Props) => { async onSubmit(values, helpers) { try { await resizeDisk(values); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar('Disk queued for resizing.', { variant: 'success' }); onClose(); } catch (e) { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx index bdaa712569d..6a4d181e61e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MutationNotification.tsx @@ -24,7 +24,7 @@ export const MutationNotification = (props: Props) => { const { data: linode } = useLinodeQuery(linodeId); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { data: currentTypeInfo } = useTypeQuery( linode?.type ?? '', @@ -52,7 +52,7 @@ export const MutationNotification = (props: Props) => { const initMutation = () => { startMutation().then(() => { setIsMutationDrawerOpen(false); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar('Linode upgrade has been initiated.', { variant: 'info', }); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx index a25d79f10f2..e0b5dd916bb 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx @@ -29,7 +29,7 @@ export const DeleteLinodeDialog = (props: Props) => { const flags = useFlags(); const { data: account } = useAccount(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const enableVPCActions = isFeatureEnabled( 'VPCs', @@ -80,7 +80,7 @@ export const DeleteLinodeDialog = (props: Props) => { }); } onClose(); - resetEventsPolling(); + checkForNewEvents(); if (onSuccess) { onSuccess(); diff --git a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx index c47f227a78a..652ad7072b3 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx @@ -54,7 +54,7 @@ export const MigrateLinode = React.memo((props: Props) => { const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { data: linode } = useLinodeQuery( linodeId ?? -1, @@ -160,7 +160,7 @@ export const MigrateLinode = React.memo((props: Props) => { return migrateLinode({ region: selectedRegion, }).then(() => { - resetEventsPolling(); + checkForNewEvents(); sendMigrationInitiatedEvent( region, selectedRegion, diff --git a/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx b/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx index 2dbf68d2018..62904264a4d 100644 --- a/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx +++ b/packages/manager/src/features/Linodes/PowerActionsDialogOrDrawer.tsx @@ -80,7 +80,7 @@ export const PowerActionsDialog = (props: Props) => { mutateAsync: shutdownLinode, } = useShutdownLinodeMutation(linodeId ?? -1); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const [selectedConfigID, setSelectConfigID] = React.useState( null @@ -119,7 +119,7 @@ export const PowerActionsDialog = (props: Props) => { const mutateAsync = mutationMap[action as 'Power Off']; await mutateAsync(); } - resetEventsPolling(); + checkForNewEvents(); onClose(); }; diff --git a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx index a6cdd657976..eafaa9d5bda 100644 --- a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx @@ -37,7 +37,7 @@ export const AttachVolumeDrawer = React.memo((props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { data: grants } = useGrants(); @@ -50,7 +50,7 @@ export const AttachVolumeDrawer = React.memo((props: Props) => { volumeId: volume?.id ?? -1, ...values, }).then(() => { - resetEventsPolling(); + checkForNewEvents(); handleClose(); enqueueSnackbar(`Volume attachment started`, { variant: 'info', diff --git a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx b/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx index 2209cba5691..b886b720a7e 100644 --- a/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/CloneVolumeDrawer.tsx @@ -31,7 +31,7 @@ export const CloneVolumeDrawer = (props: Props) => { const { mutateAsync: cloneVolume } = useCloneVolumeMutation(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { data: grants } = useGrants(); @@ -58,7 +58,7 @@ export const CloneVolumeDrawer = (props: Props) => { try { await cloneVolume({ label: values.label, volumeId: volume?.id ?? -1 }); onClose(); - resetEventsPolling(); + checkForNewEvents(); } catch (error) { handleFieldErrors(setErrors, error); handleGeneralErrors( diff --git a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx b/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx index 8ddde52041b..762c4bc97c6 100644 --- a/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DeleteVolumeDialog.tsx @@ -21,12 +21,12 @@ export const DeleteVolumeDialog = (props: Props) => { mutateAsync: deleteVolume, } = useDeleteVolumeMutation(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const onDelete = () => { deleteVolume({ id: volume?.id ?? -1 }).then(() => { onClose(); - resetEventsPolling(); + checkForNewEvents(); }); }; diff --git a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx index cb944f90e13..6e9011a7cb7 100644 --- a/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DetachVolumeDialog.tsx @@ -20,7 +20,7 @@ export const DetachVolumeDialog = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const { data: linode } = useLinodeQuery( volume?.linode_id ?? -1, @@ -36,7 +36,7 @@ export const DetachVolumeDialog = (props: Props) => { const onDetach = () => { detachVolume({ id: volume?.id ?? -1 }).then(() => { onClose(); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar(`Volume detachment started`, { variant: 'info', }); diff --git a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx b/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx index 56cc101b3fd..01bd737889a 100644 --- a/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/ResizeVolumeDrawer.tsx @@ -29,7 +29,7 @@ export const ResizeVolumeDrawer = (props: Props) => { const { mutateAsync: resizeVolume } = useResizeVolumeMutation(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const validationSchema = ResizeVolumeSchema(volume?.size ?? -1); @@ -61,7 +61,7 @@ export const ResizeVolumeDrawer = (props: Props) => { resizeVolume({ size: Number(values.size), volumeId: volume?.id ?? -1 }) .then((_) => { setSubmitting(false); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar(`Volume scheduled to be resized.`, { variant: 'success', }); diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx index b477906b011..229c2da924f 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeAttachForm.tsx @@ -46,7 +46,7 @@ export const LinodeVolumeAttachForm = (props: Props) => { const { enqueueSnackbar } = useSnackbar(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const linodeGrant = grants?.linode.find( (grant: Grant) => grant.id === linode.id @@ -76,7 +76,7 @@ export const LinodeVolumeAttachForm = (props: Props) => { volumeId: values.volume_id, }); onClose(); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar(`Volume attachment started`, { variant: 'info', }); diff --git a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx index 473ec6eea94..30debabe284 100644 --- a/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeDrawer/LinodeVolumeCreateForm.tsx @@ -57,7 +57,7 @@ export const LinodeVolumeCreateForm = (props: Props) => { const { data: grants } = useGrants(); const { mutateAsync: createVolume } = useCreateVolumeMutation(); - const { resetEventsPolling } = useEventsPollingActions(); + const { checkForNewEvents } = useEventsPollingActions(); const disabled = profile?.restricted && !grants?.global.add_volumes; @@ -90,7 +90,7 @@ export const LinodeVolumeCreateForm = (props: Props) => { size: maybeCastToNumber(size), tags, }); - resetEventsPolling(); + checkForNewEvents(); enqueueSnackbar(`Volume scheduled for creation.`, { variant: 'success', }); diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index b0d0122806f..fae14229d00 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -173,16 +173,22 @@ export const useEventsPoller = () => { export const useEventsPollingActions = () => { const queryClient = useQueryClient(); - const resetEventsPolling = () => + const checkForNewEvents = () => { + // Invalidating the event poller will cause useEventsPoller's `queryFn` + // to re-run and pull down any new events. queryClient.invalidateQueries(['events', 'poller']); + }; return { /** - * Sets the polling interval to 1 second so that events get polled faster temporarily + * Makes a request to `/v4/account/events` to check for any new events. * - * The polling backoff will start over from 1 second. + * This function is intended to be called *after* initiating a long-running + * event (like Linode create) so that Cloud Manager sees the new event and starts + * polling at a faster rate. We do this to give Cloud Manager users a more + * "realtime" feeling experience. */ - resetEventsPolling, + checkForNewEvents, }; }; From 3c8886a0344ddee2c802d5a8d556c3839351025a Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:11:51 -0500 Subject: [PATCH 43/44] Apply suggestions from @jdamore-linode Co-authored-by: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> --- packages/manager/src/queries/events/event.helpers.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/queries/events/event.helpers.test.ts b/packages/manager/src/queries/events/event.helpers.test.ts index 3c1221a8fd8..bb8a45b671c 100644 --- a/packages/manager/src/queries/events/event.helpers.test.ts +++ b/packages/manager/src/queries/events/event.helpers.test.ts @@ -115,21 +115,21 @@ describe('doesEventMatchAPIFilter', () => { expect(doesEventMatchAPIFilter(event, filter)).toBe(true); }); - it('should return true because the incomming entity matches the API filter', () => { + it('should return true because the incoming entity matches the API filter', () => { const event = eventFactory.build({ entity: { id: 1, type: 'linode' } }); const filter = { 'entity.id': 1, 'entity.type': 'linode' }; expect(doesEventMatchAPIFilter(event, filter)).toBe(true); }); - it('should return false because the incomming event does not match the API filter', () => { + it('should return false because the incoming event does not match the API filter', () => { const event = eventFactory.build({ entity: { id: 1, type: 'linode' } }); const filter = { 'entity.id': 2, 'entity.type': 'linode' }; expect(doesEventMatchAPIFilter(event, filter)).toBe(false); }); - it('should return false because the incomming event does not match the API filter', () => { + it('should return false because the incoming event does not match the API filter', () => { const event = eventFactory.build({ entity: null }); const filter = { 'entity.id': 2, 'entity.type': 'linode' }; From 97b7455cecaebc712f36be2054ecd376f841f7c7 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:13:12 -0500 Subject: [PATCH 44/44] Update packages/manager/src/queries/events/events.ts --- packages/manager/src/queries/events/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/queries/events/events.ts b/packages/manager/src/queries/events/events.ts index fae14229d00..ba2279ef3da 100644 --- a/packages/manager/src/queries/events/events.ts +++ b/packages/manager/src/queries/events/events.ts @@ -266,7 +266,7 @@ export const updateEventsQueries = ( /** * Updates a events infinite query with incoming events from our polling * - * This method should do two things with incomming events + * This method should do two things with incoming events * - If the events is already in the cache, update it * - If the event is new, append it to the top of the first page. */