alertStore.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import type {Theme} from '@emotion/react';
  2. import {createStore} from 'reflux';
  3. import {defined} from 'sentry/utils';
  4. import localStorage from 'sentry/utils/localStorage';
  5. import type {StrictStoreDefinition} from './types';
  6. type Alert = {
  7. message: React.ReactNode;
  8. type: keyof Theme['alert'];
  9. expireAfter?: number;
  10. id?: string;
  11. key?: number;
  12. neverExpire?: boolean;
  13. noDuplicates?: boolean;
  14. onClose?: () => void;
  15. opaque?: boolean;
  16. url?: string;
  17. };
  18. interface InternalAlertStoreDefinition {
  19. count: number;
  20. }
  21. interface AlertStoreDefinition
  22. extends StrictStoreDefinition<Alert[]>,
  23. InternalAlertStoreDefinition {
  24. addAlert(alert: Alert): void;
  25. closeAlert(alert: Alert, duration?: number): void;
  26. }
  27. const storeConfig: AlertStoreDefinition = {
  28. state: [],
  29. count: 0,
  30. init() {
  31. // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux
  32. // listeners due to their leaky nature in tests.
  33. this.state = [];
  34. this.count = 0;
  35. },
  36. addAlert(alert) {
  37. const alertAlreadyExists = this.state.some(a => a.id === alert.id);
  38. if (alertAlreadyExists && alert.noDuplicates) {
  39. return;
  40. }
  41. if (defined(alert.id)) {
  42. const mutedData = localStorage.getItem('alerts:muted');
  43. if (typeof mutedData === 'string' && mutedData.length) {
  44. const expirations: Record<string, number> = JSON.parse(mutedData);
  45. // Remove any objects that have passed their mute duration.
  46. const now = Math.floor(new Date().valueOf() / 1000);
  47. for (const key in expirations) {
  48. if (expirations.hasOwnProperty(key) && expirations[key] < now) {
  49. delete expirations[key];
  50. }
  51. }
  52. localStorage.setItem('alerts:muted', JSON.stringify(expirations));
  53. if (expirations.hasOwnProperty(alert.id)) {
  54. return;
  55. }
  56. }
  57. } else {
  58. if (!defined(alert.expireAfter)) {
  59. alert.expireAfter = 5000;
  60. }
  61. }
  62. if (alert.expireAfter && !alert.neverExpire) {
  63. window.setTimeout(() => {
  64. this.closeAlert(alert);
  65. }, alert.expireAfter);
  66. }
  67. alert.key = this.count++;
  68. // intentionally recreate array via concat because of Reflux
  69. // "bug" where React components are given same reference to tracked
  70. // data objects, and don't *see* that values have changed
  71. this.state = this.state.concat([alert]);
  72. this.trigger(this.state);
  73. },
  74. closeAlert(alert, duration = 60 * 60 * 7 * 24) {
  75. if (defined(alert.id) && defined(duration)) {
  76. const expiry = Math.floor(new Date().valueOf() / 1000) + duration;
  77. const mutedData = localStorage.getItem('alerts:muted');
  78. let expirations: Record<string, number> = {};
  79. if (typeof mutedData === 'string' && expirations.length) {
  80. expirations = JSON.parse(mutedData);
  81. }
  82. expirations[alert.id] = expiry;
  83. localStorage.setItem('alerts:muted', JSON.stringify(expirations));
  84. }
  85. // TODO(dcramer): we need some animations here for closing alerts
  86. this.state = this.state.filter(item => alert !== item);
  87. this.trigger(this.state);
  88. },
  89. getState() {
  90. return this.state;
  91. },
  92. };
  93. const AlertStore = createStore(storeConfig);
  94. export default AlertStore;