pageFiltersStore.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import {createStore} from 'reflux';
  2. import {getDefaultSelection} from 'sentry/components/organizations/pageFilters/utils';
  3. import type {PageFilters, PinnedPageFilter} from 'sentry/types';
  4. import type {CommonStoreDefinition} from './types';
  5. function datetimeHasSameValue(
  6. a: PageFilters['datetime'],
  7. b: PageFilters['datetime']
  8. ): boolean {
  9. if (Object.keys(a).length !== Object.keys(b).length) {
  10. return false;
  11. }
  12. for (const key in a) {
  13. if (a[key] instanceof Date && b[key] instanceof Date) {
  14. // This will fail on invalid dates as NaN !== NaN,
  15. // but thats fine since we don't want invalid dates to be equal
  16. if (a[key].getTime() === b[key].getTime()) {
  17. continue;
  18. }
  19. return false;
  20. }
  21. if (a[key] === null && b[key] === null) {
  22. continue;
  23. }
  24. if (a[key] !== b[key]) {
  25. return false;
  26. }
  27. }
  28. return true;
  29. }
  30. function arrayIsEqual(
  31. a: string[] | number[] | null,
  32. b: string[] | number[] | null
  33. ): boolean {
  34. if (a === null && b === null) {
  35. return true;
  36. }
  37. if (Array.isArray(a) && Array.isArray(b)) {
  38. if (a.length !== b.length) {
  39. return false;
  40. }
  41. if (a.length === 1 && b.length === 1) {
  42. return a[0] === b[0];
  43. }
  44. return a.every((value, index) => value === b[index]);
  45. }
  46. return a === b;
  47. }
  48. interface CommonState {
  49. /**
  50. * The set of page filters which have been pinned but do not match the current
  51. * URL state.
  52. */
  53. desyncedFilters: Set<PinnedPageFilter>;
  54. /**
  55. * The set of page filters which are currently pinned
  56. */
  57. pinnedFilters: Set<PinnedPageFilter>;
  58. /**
  59. * The current page filter selection
  60. */
  61. selection: PageFilters;
  62. /**
  63. * Whether to save changes to local storage. This setting should be page-specific:
  64. * most pages should have it on (default) and some, like Dashboard Details, need it
  65. * off.
  66. */
  67. shouldPersist: boolean;
  68. }
  69. /**
  70. * External state
  71. */
  72. interface State extends CommonState {
  73. /**
  74. * Are page filters ready?
  75. */
  76. isReady: boolean;
  77. }
  78. interface InternalDefinition extends CommonState {
  79. /**
  80. * Have we initalized page filters?
  81. */
  82. hasInitialState: boolean;
  83. }
  84. interface PageFiltersStoreDefinition
  85. extends InternalDefinition,
  86. CommonStoreDefinition<State> {
  87. init(): void;
  88. onInitializeUrlState(
  89. newSelection: PageFilters,
  90. pinned: Set<PinnedPageFilter>,
  91. persist?: boolean
  92. ): void;
  93. onReset(): void;
  94. pin(filter: PinnedPageFilter, pin: boolean): void;
  95. reset(selection?: PageFilters): void;
  96. updateDateTime(datetime: PageFilters['datetime']): void;
  97. updateDesyncedFilters(filters: Set<PinnedPageFilter>): void;
  98. updateEnvironments(environments: string[] | null): void;
  99. updatePersistence(shouldPersist: boolean): void;
  100. updateProjects(projects: PageFilters['projects'], environments: null | string[]): void;
  101. }
  102. const storeConfig: PageFiltersStoreDefinition = {
  103. selection: getDefaultSelection(),
  104. pinnedFilters: new Set(),
  105. desyncedFilters: new Set(),
  106. shouldPersist: true,
  107. hasInitialState: false,
  108. init() {
  109. // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux
  110. // listeners due to their leaky nature in tests.
  111. this.reset(this.selection);
  112. },
  113. reset(selection) {
  114. this._isReady = false;
  115. this.selection = selection || getDefaultSelection();
  116. this.pinnedFilters = new Set();
  117. },
  118. /**
  119. * Initializes the page filters store data
  120. */
  121. onInitializeUrlState(newSelection, pinned, persist = true) {
  122. this._isReady = true;
  123. this.selection = newSelection;
  124. this.pinnedFilters = pinned;
  125. this.shouldPersist = persist;
  126. this.trigger(this.getState());
  127. },
  128. getState() {
  129. return {
  130. selection: this.selection,
  131. pinnedFilters: this.pinnedFilters,
  132. desyncedFilters: this.desyncedFilters,
  133. shouldPersist: this.shouldPersist,
  134. isReady: this._isReady,
  135. };
  136. },
  137. onReset() {
  138. this.reset();
  139. this.trigger(this.getState());
  140. },
  141. updatePersistence(shouldPersist: boolean) {
  142. this.shouldPersist = shouldPersist;
  143. this.trigger(this.getState());
  144. },
  145. updateDesyncedFilters(filters: Set<PinnedPageFilter>) {
  146. this.desyncedFilters = filters;
  147. this.trigger(this.getState());
  148. },
  149. updateProjects(projects = [], environments = null) {
  150. if (arrayIsEqual(this.selection.projects, projects)) {
  151. return;
  152. }
  153. if (this.desyncedFilters.has('projects')) {
  154. const newDesyncedFilters = new Set(this.desyncedFilters);
  155. newDesyncedFilters.delete('projects');
  156. this.desyncedFilters = newDesyncedFilters;
  157. }
  158. this.selection = {
  159. ...this.selection,
  160. projects,
  161. environments: environments === null ? this.selection.environments : environments,
  162. };
  163. this.trigger(this.getState());
  164. },
  165. updateDateTime(newDateTime) {
  166. if (datetimeHasSameValue(this.selection.datetime, newDateTime)) {
  167. return;
  168. }
  169. if (this.desyncedFilters.has('datetime')) {
  170. const newDesyncedFilters = new Set(this.desyncedFilters);
  171. newDesyncedFilters.delete('datetime');
  172. this.desyncedFilters = newDesyncedFilters;
  173. }
  174. this.selection = {
  175. ...this.selection,
  176. datetime: newDateTime,
  177. };
  178. this.trigger(this.getState());
  179. },
  180. updateEnvironments(environments) {
  181. if (arrayIsEqual(this.selection.environments, environments)) {
  182. return;
  183. }
  184. if (this.desyncedFilters.has('environments')) {
  185. const newDesyncedFilters = new Set(this.desyncedFilters);
  186. newDesyncedFilters.delete('environments');
  187. this.desyncedFilters = newDesyncedFilters;
  188. }
  189. this.selection = {
  190. ...this.selection,
  191. environments: environments ?? [],
  192. };
  193. this.trigger(this.getState());
  194. },
  195. pin(filter, pin) {
  196. if (pin) {
  197. this.pinnedFilters.add(filter);
  198. } else {
  199. this.pinnedFilters.delete(filter);
  200. }
  201. this.trigger(this.getState());
  202. },
  203. };
  204. const PageFiltersStore = createStore(storeConfig);
  205. export default PageFiltersStore;