, APIError[]>(
+ ['events', 'infinite', filter],
+ ({ pageParam }) =>
+ getEvents(
+ {},
+ { ...filter, id: pageParam ? { '+lt': pageParam } : undefined }
+ ),
+ {
+ cacheTime: Infinity,
+ getNextPageParam: ({ data, results }) => {
+ if (results === data.length) {
+ return undefined;
+ }
+ return data[data.length - 1].id;
+ },
+ staleTime: Infinity,
+ }
+ );
+
+ const events = query.data?.pages.reduce(
+ (events, page) => [...events, ...page.data],
+ []
+ );
+
+ 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({
+ enabled: false,
+ queryKey: ['events', 'poller'],
+ });
+};
+
+/**
+ * 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 { handleGlobalToast } = useToastNotifications();
+ const { handleEvent } = useEventHandlers();
+
+ const queryClient = useQueryClient();
+
+ const { events } = useEventsInfiniteQuery();
+
+ const hasFetchedInitialEvents = events !== undefined;
+
+ const mountTimestamp = useRef(
+ DateTime.now().setZone('utc').toFormat(ISO_DATETIME_NO_TZ_FORMAT)
+ );
+
+ useQuery({
+ enabled: hasFetchedInitialEvents,
+ onSuccess(events) {
+ if (events.length > 0) {
+ updateEventsQueries(events, queryClient);
+
+ for (const event of events) {
+ handleGlobalToast(event);
+ handleEvent(event);
+ }
+ }
+ },
+ 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.current;
+
+ const {
+ eventsThatAlreadyHappenedAtTheFilterTime,
+ inProgressEvents,
+ } = getExistingEventDataForPollingFilterGenerator(
+ events,
+ latestEventTime
+ );
+
+ const filter = generatePollingFilter(
+ latestEventTime,
+ inProgressEvents,
+ eventsThatAlreadyHappenedAtTheFilterTime
+ );
+
+ return getEvents({}, filter).then((data) => data.data);
+ },
+ queryKey: ['events', 'poller'],
+ refetchInterval: (data) => {
+ const hasInProgressEvents = data?.some(isInProgressEvent);
+ if (hasInProgressEvents) {
+ return POLLING_INTERVALS.IN_PROGRESS;
+ }
+ return POLLING_INTERVALS.DEFAULT;
+ },
+ });
+
+ return null;
+};
+
+/**
+ * This hook returns functions that allow us to interact
+ * with our events polling system.
+ */
+export const useEventsPollingActions = () => {
+ const queryClient = useQueryClient();
+
+ 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 {
+ /**
+ * Makes a request to `/v4/account/events` to check for any new events.
+ *
+ * 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.
+ */
+ checkForNewEvents,
+ };
+};
+
+export const useMarkEventsAsSeen = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, APIError[], number>(
+ (eventId) => markEventSeen(eventId),
+ {
+ onSuccess: (_, eventId) => {
+ queryClient.setQueryData>>(
+ ['events', 'infinite', undefined],
+ (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 ?? [],
+ };
+ }
+ );
+ },
+ }
+ );
+};
+
+/**
+ * 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
+) => {
+ 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) => {
+ return doesEventMatchAPIFilter(event, apiFilter);
+ });
+
+ updateEventsQuery(filteredEvents, queryKey, queryClient);
+ });
+};
+
+/**
+ * Updates a events infinite query with incoming events from our polling
+ *
+ * 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.
+ */
+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,
+ };
+ }
+ );
+};
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 0f7338c6067..2e73804202d 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.
@@ -111,7 +114,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 444805429b6..dc11cf2a47d 100644
--- a/packages/manager/src/queries/nodebalancers.ts
+++ b/packages/manager/src/queries/nodebalancers.ts
@@ -31,7 +31,7 @@ import {
useQueryClient,
} from 'react-query';
-import { EventWithStore } from 'src/events';
+import { EventHandlerData } from 'src/hooks/useEventHandlers';
import { queryKey as firewallsQueryKey } from 'src/queries/firewalls';
import { parseAPIDate } from 'src/utilities/date';
import { getAll } from 'src/utilities/getAll';
@@ -207,7 +207,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 93b95599dd5..a95c92040a4 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';
import type { RequestOptions } from '@linode/api-v4';
@@ -160,7 +159,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 55f0fc7be16..8c63b66e11c 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
deleted file mode 100644
index 306a071ab84..00000000000
--- a/packages/manager/src/store/events/event.helpers.test.ts
+++ /dev/null
@@ -1,351 +0,0 @@
-import { Event } from '@linode/api-v4/lib/account';
-import { DateTime } from 'luxon';
-
-import { eventFactory } from 'src/factories/events';
-
-import {
- addToEvents,
- findInEvents,
- getNumUnseenEvents,
- isCompletedEvent,
- isInProgressEvent,
- mostRecentCreated,
- setDeletedEvents,
- updateInProgressEvents,
-} from './event.helpers';
-import { ExtendedEvent } from './event.types';
-
-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('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);
- });
- });
-
- 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('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();
- });
- });
-
- 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);
- });
- });
-
- 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: ExtendedEvent[] = [
- {
- _deleted: '2018-12-02T23:15:45',
- 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',
- },
- {
- _deleted: '2018-12-02T23:15:45',
- 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);
- });
- });
-
- 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',
- },
- ]);
- });
- });
-
- 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 });
- });
- });
-});
diff --git a/packages/manager/src/store/events/event.helpers.ts b/packages/manager/src/store/events/event.helpers.ts
deleted file mode 100644
index ac2776f5796..00000000000
--- a/packages/manager/src/store/events/event.helpers.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-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';
-
-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();
-
-/**
- * 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);
-
-export const isLongPendingEvent = (event: Event): boolean => {
- const { action, status } = event;
- return status === 'scheduled' && action === 'image_upload';
-};
-
-export const isInProgressEvent = (event: Event) => {
- const { percent_complete } = event;
- if (percent_complete === null || isLongPendingEvent(event)) {
- return false;
- } else {
- return percent_complete !== null && percent_complete < 100;
- }
-};
-
-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 isEntityEvent = (e: Event): e is EntityEvent => Boolean(e.entity);
-
-export const isEventInProgressDiskImagize = (event: Event): boolean => {
- return (
- event.action === 'disk_imagize' &&
- Boolean(event.secondary_entity) &&
- isInProgressEvent(event)
- );
-};
-
-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);
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..2728fe0b015 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, applyMiddleware, 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,
@@ -46,13 +33,11 @@ import pendingUpload, {
State as PendingUploadState,
defaultState as pendingUploadState,
} from './pendingUpload';
-
-const reduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION__;
+import thunk from 'redux-thunk';
export interface ApplicationState {
authentication: AuthState;
createLinode: LinodeCreateState;
- events: EventsState;
globalErrors: GlobalErrorState;
longviewClients: LongviewState;
longviewStats: LongviewStatsState;
@@ -64,7 +49,6 @@ export interface ApplicationState {
export const defaultState: ApplicationState = {
authentication: authenticationDefaultState,
createLinode: linodeCreateDefaultState,
- events: eventsDefaultState,
globalErrors: defaultGlobalErrorState,
longviewClients: defaultLongviewState,
longviewStats: defaultLongviewStatsState,
@@ -79,7 +63,6 @@ export const defaultState: ApplicationState = {
const reducers = combineReducers({
authentication,
createLinode: linodeCreateReducer,
- events,
globalErrors,
longviewClients: longview,
longviewStats,
@@ -88,14 +71,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, applyMiddleware(thunk));
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/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;
- }
- );
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;
}
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,
- });
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/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;
-};
diff --git a/packages/manager/src/utilities/testHelpers.tsx b/packages/manager/src/utilities/testHelpers.tsx
index 05aba12a843..0f872368071 100644
--- a/packages/manager/src/utilities/testHelpers.tsx
+++ b/packages/manager/src/utilities/testHelpers.tsx
@@ -82,9 +82,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"
@@ -132,8 +130,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 41e735cd4e7..9db8480f628 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13404,13 +13404,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"
@@ -14176,11 +14169,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"