alertStore.tsx 3.2 KB

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