appNotification.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import { createSlice, PayloadAction } from '@reduxjs/toolkit';
  2. import { AppNotification, AppNotificationSeverity, AppNotificationsState } from 'app/types/';
  3. const MAX_STORED_NOTIFICATIONS = 25;
  4. export const STORAGE_KEY = 'notifications';
  5. export const NEW_NOTIFS_KEY = `${STORAGE_KEY}/lastRead`;
  6. type StoredNotification = Omit<AppNotification, 'component'>;
  7. export const initialState: AppNotificationsState = {
  8. byId: deserializeNotifications(),
  9. lastRead: Number.parseInt(window.localStorage.getItem(NEW_NOTIFS_KEY) ?? `${Date.now()}`, 10),
  10. };
  11. /**
  12. * Reducer and action to show toast notifications of various types (success, warnings, errors etc). Use to show
  13. * transient info to user, like errors that cannot be otherwise handled or success after an action.
  14. *
  15. * Use factory functions in core/copy/appNotifications to create the payload.
  16. */
  17. const appNotificationsSlice = createSlice({
  18. name: 'appNotifications',
  19. initialState,
  20. reducers: {
  21. notifyApp: (state, { payload: newAlert }: PayloadAction<AppNotification>) => {
  22. if (Object.values(state.byId).some((alert) => isSimilar(newAlert, alert) && alert.showing)) {
  23. return;
  24. }
  25. state.byId[newAlert.id] = newAlert;
  26. serializeNotifications(state.byId);
  27. },
  28. hideAppNotification: (state, { payload: alertId }: PayloadAction<string>) => {
  29. if (!(alertId in state.byId)) {
  30. return;
  31. }
  32. state.byId[alertId].showing = false;
  33. serializeNotifications(state.byId);
  34. },
  35. clearNotification: (state, { payload: alertId }: PayloadAction<string>) => {
  36. delete state.byId[alertId];
  37. serializeNotifications(state.byId);
  38. },
  39. clearAllNotifications: (state) => {
  40. state.byId = {};
  41. serializeNotifications(state.byId);
  42. },
  43. readAllNotifications: (state, { payload: timestamp }: PayloadAction<number>) => {
  44. state.lastRead = timestamp;
  45. },
  46. },
  47. });
  48. export const { notifyApp, hideAppNotification, clearNotification, clearAllNotifications, readAllNotifications } =
  49. appNotificationsSlice.actions;
  50. export const appNotificationsReducer = appNotificationsSlice.reducer;
  51. // Selectors
  52. export const selectLastReadTimestamp = (state: AppNotificationsState) => state.lastRead;
  53. export const selectAll = (state: AppNotificationsState) =>
  54. Object.values(state.byId).sort((a, b) => b.timestamp - a.timestamp);
  55. export const selectWarningsAndErrors = (state: AppNotificationsState) => selectAll(state).filter(isAtLeastWarning);
  56. export const selectVisible = (state: AppNotificationsState) => Object.values(state.byId).filter((n) => n.showing);
  57. // Helper functions
  58. function isSimilar(a: AppNotification, b: AppNotification): boolean {
  59. return a.icon === b.icon && a.severity === b.severity && a.text === b.text && a.title === b.title;
  60. }
  61. function isAtLeastWarning(notif: AppNotification) {
  62. return notif.severity === AppNotificationSeverity.Warning || notif.severity === AppNotificationSeverity.Error;
  63. }
  64. function isStoredNotification(obj: any): obj is StoredNotification {
  65. return (
  66. typeof obj.id === 'string' &&
  67. typeof obj.icon === 'string' &&
  68. typeof obj.title === 'string' &&
  69. typeof obj.text === 'string'
  70. );
  71. }
  72. // (De)serialization
  73. export function deserializeNotifications(): Record<string, StoredNotification> {
  74. const storedNotifsRaw = window.localStorage.getItem(STORAGE_KEY);
  75. if (!storedNotifsRaw) {
  76. return {};
  77. }
  78. const parsed = JSON.parse(storedNotifsRaw);
  79. if (!Object.values(parsed).every((v) => isStoredNotification(v))) {
  80. return {};
  81. }
  82. return parsed;
  83. }
  84. function serializeNotifications(notifs: Record<string, StoredNotification>) {
  85. const reducedNotifs = Object.values(notifs)
  86. .filter(isAtLeastWarning)
  87. .sort((a, b) => b.timestamp - a.timestamp)
  88. .slice(0, MAX_STORED_NOTIFICATIONS)
  89. .reduce<Record<string, StoredNotification>>((prev, cur) => {
  90. prev[cur.id] = {
  91. id: cur.id,
  92. severity: cur.severity,
  93. icon: cur.icon,
  94. title: cur.title,
  95. text: cur.text,
  96. traceId: cur.traceId,
  97. timestamp: cur.timestamp,
  98. showing: cur.showing,
  99. };
  100. return prev;
  101. }, {});
  102. try {
  103. window.localStorage.setItem(STORAGE_KEY, JSON.stringify(reducedNotifs));
  104. } catch (err) {
  105. console.error('Unable to persist notifications to local storage');
  106. console.error(err);
  107. }
  108. }