pageFilters.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. import type {InjectedRouter} from 'react-router';
  2. import * as Sentry from '@sentry/react';
  3. import type {Location} from 'history';
  4. import isInteger from 'lodash/isInteger';
  5. import omit from 'lodash/omit';
  6. import pick from 'lodash/pick';
  7. import * as qs from 'query-string';
  8. import {
  9. getDatetimeFromState,
  10. getStateFromQuery,
  11. } from 'sentry/components/organizations/pageFilters/parse';
  12. import {
  13. getPageFilterStorage,
  14. setPageFiltersStorage,
  15. } from 'sentry/components/organizations/pageFilters/persistence';
  16. import type {PageFiltersStringified} from 'sentry/components/organizations/pageFilters/types';
  17. import {getDefaultSelection} from 'sentry/components/organizations/pageFilters/utils';
  18. import {
  19. ALL_ACCESS_PROJECTS,
  20. DATE_TIME_KEYS,
  21. URL_PARAM,
  22. } from 'sentry/constants/pageFilters';
  23. import OrganizationStore from 'sentry/stores/organizationStore';
  24. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  25. import type {
  26. DateString,
  27. Environment,
  28. MinimalProject,
  29. Organization,
  30. PageFilters,
  31. PinnedPageFilter,
  32. Project,
  33. } from 'sentry/types';
  34. import {defined, valueIsEqual} from 'sentry/utils';
  35. import {getUtcDateString} from 'sentry/utils/dates';
  36. type EnvironmentId = Environment['id'];
  37. type Options = {
  38. /**
  39. * Do not reset the `cursor` query parameter when updating page filters
  40. */
  41. keepCursor?: boolean;
  42. /**
  43. * Use Location.replace instead of push when updating the URL query state
  44. */
  45. replace?: boolean;
  46. /**
  47. * List of parameters to remove when changing URL params
  48. */
  49. resetParams?: string[];
  50. /**
  51. * Persist changes to the page filter selection into local storage
  52. */
  53. save?: boolean;
  54. /**
  55. * Optional prefix for the storage key, for areas of the app that need separate pagefilters (i.e Starfish)
  56. */
  57. storageNamespace?: string;
  58. };
  59. /**
  60. * This is the 'update' object used for updating the page filters. The types
  61. * here are a bit wider to allow for easy updates.
  62. */
  63. type PageFiltersUpdate = {
  64. end?: DateString;
  65. environment?: string[] | null;
  66. period?: string | null;
  67. project?: number[] | null;
  68. start?: DateString;
  69. utc?: boolean | null;
  70. };
  71. /**
  72. * Represents the input for updating the date time of page filters
  73. */
  74. type DateTimeUpdate = Pick<PageFiltersUpdate, 'start' | 'end' | 'period' | 'utc'>;
  75. /**
  76. * Output object used for updating query parameters
  77. */
  78. type PageFilterQuery = PageFiltersStringified & Record<string, Location['query'][string]>;
  79. /**
  80. * This can be null which will not perform any router side effects, and instead updates store.
  81. */
  82. type Router = InjectedRouter | null | undefined;
  83. /**
  84. * Reset values in the page filters store
  85. */
  86. export function resetPageFilters() {
  87. PageFiltersStore.onReset();
  88. }
  89. function getProjectIdFromProject(project: MinimalProject) {
  90. return parseInt(project.id, 10);
  91. }
  92. /**
  93. * Merges two date time objects, where the `base` object takes precedence, and
  94. * the `fallback` values are used when the base values are null or undefined.
  95. */
  96. function mergeDatetime(
  97. base: PageFilters['datetime'],
  98. fallback?: Partial<PageFilters['datetime']>
  99. ) {
  100. const datetime: PageFilters['datetime'] = {
  101. start: base.start ?? fallback?.start ?? null,
  102. end: base.end ?? fallback?.end ?? null,
  103. period: base.period ?? fallback?.period ?? null,
  104. utc: base.utc ?? fallback?.utc ?? null,
  105. };
  106. return datetime;
  107. }
  108. export type InitializeUrlStateParams = {
  109. memberProjects: Project[];
  110. nonMemberProjects: Project[];
  111. organization: Organization;
  112. queryParams: Location['query'];
  113. router: InjectedRouter;
  114. shouldEnforceSingleProject: boolean;
  115. defaultSelection?: Partial<PageFilters>;
  116. forceProject?: MinimalProject | null;
  117. shouldForceProject?: boolean;
  118. /**
  119. * Whether to save changes to local storage. This setting should be page-specific:
  120. * most pages should have it on (default) and some, like Dashboard Details, need it
  121. * off.
  122. */
  123. shouldPersist?: boolean;
  124. showAbsolute?: boolean;
  125. /**
  126. * When used with shouldForceProject it will not persist the project id
  127. * to url query parameters on load. This is useful when global selection header
  128. * is used for display purposes rather than selection.
  129. */
  130. skipInitializeUrlParams?: boolean;
  131. /**
  132. * Skip loading from local storage
  133. * An example is Issue Details, in the case where it is accessed directly (e.g. from email).
  134. * We do not want to load the user's last used env/project in this case, otherwise will
  135. * lead to very confusing behavior.
  136. */
  137. skipLoadLastUsed?: boolean;
  138. /**
  139. * Skip loading last used environment from local storage
  140. * An example is Starfish, which doesn't support environments.
  141. */
  142. skipLoadLastUsedEnvironment?: boolean;
  143. /**
  144. * Optional prefix for the storage key, for areas of the app that need separate pagefilters (i.e Starfish)
  145. */
  146. storageNamespace?: string;
  147. };
  148. export function initializeUrlState({
  149. organization,
  150. queryParams,
  151. router,
  152. memberProjects,
  153. nonMemberProjects,
  154. skipLoadLastUsed,
  155. skipLoadLastUsedEnvironment,
  156. shouldPersist = true,
  157. shouldForceProject,
  158. shouldEnforceSingleProject,
  159. defaultSelection,
  160. forceProject,
  161. showAbsolute = true,
  162. skipInitializeUrlParams = false,
  163. storageNamespace,
  164. }: InitializeUrlStateParams) {
  165. const orgSlug = organization.slug;
  166. const parsed = getStateFromQuery(queryParams, {
  167. allowAbsoluteDatetime: showAbsolute,
  168. allowEmptyPeriod: true,
  169. });
  170. const {datetime: defaultDatetime, ...defaultFilters} = getDefaultSelection();
  171. const {datetime: customDatetime, ...customDefaultFilters} = defaultSelection ?? {};
  172. const pageFilters: PageFilters = {
  173. ...defaultFilters,
  174. ...customDefaultFilters,
  175. datetime: mergeDatetime(parsed, customDatetime),
  176. };
  177. // Use period from default if we don't have a period set
  178. pageFilters.datetime.period ??= defaultDatetime.period;
  179. // Do not set a period if we have absolute start and end
  180. if (pageFilters.datetime.start && pageFilters.datetime.end) {
  181. pageFilters.datetime.period = null;
  182. }
  183. const hasDatetimeInUrl = Object.keys(pick(queryParams, DATE_TIME_KEYS)).length > 0;
  184. const hasProjectOrEnvironmentInUrl =
  185. Object.keys(pick(queryParams, [URL_PARAM.PROJECT, URL_PARAM.ENVIRONMENT])).length > 0;
  186. // We should only check and update the desync state if the site has just been loaded
  187. // (not counting route changes). To check this, we can use the `isReady` state: if it's
  188. // false, then the site was just loaded. Once it's true, `isReady` stays true
  189. // through route changes.
  190. let shouldCheckDesyncedURLState = !PageFiltersStore.getState().isReady;
  191. /**
  192. * Check to make sure that the project ID exists in the projects list. Invalid project
  193. * IDs (project was deleted/moved to another org) can still exist in local storage or
  194. * shared links.
  195. */
  196. function validateProjectId(projectId: number): boolean {
  197. if (projectId === ALL_ACCESS_PROJECTS) {
  198. return !shouldEnforceSingleProject;
  199. }
  200. return (
  201. !!memberProjects?.some(mp => String(mp.id) === String(projectId)) ||
  202. !!nonMemberProjects?.some(nmp => String(nmp.id) === String(projectId))
  203. );
  204. }
  205. /**
  206. * Check to make sure that the environment exists. Invalid environments (due to being
  207. * hidden) can still exist in local storage or shared links.
  208. */
  209. function validateEnvironment(env: string): boolean {
  210. return (
  211. !!memberProjects?.some(mp => mp.environments.includes(env)) ||
  212. !!nonMemberProjects?.some(nmp => nmp.environments.includes(env))
  213. );
  214. }
  215. if (hasProjectOrEnvironmentInUrl) {
  216. pageFilters.projects = parsed.project?.filter(validateProjectId) || [];
  217. pageFilters.environments = parsed.environment?.filter(validateEnvironment) || [];
  218. if (
  219. pageFilters.projects.length < (parsed.project?.length ?? 0) ||
  220. pageFilters.environments.length < (parsed.environment?.length ?? 0)
  221. ) {
  222. // don't check desync state since we're going to remove invalid projects/envs from
  223. // the URL query
  224. shouldCheckDesyncedURLState = false;
  225. }
  226. }
  227. const storedPageFilters = skipLoadLastUsed
  228. ? null
  229. : getPageFilterStorage(orgSlug, storageNamespace);
  230. let shouldUsePinnedDatetime = false;
  231. let shouldUpdateLocalStorage = false;
  232. // We may want to restore some page filters from local storage. In the new
  233. // world when they are pinned, and in the old world as long as
  234. // skipLoadLastUsed is not set to true.
  235. if (storedPageFilters) {
  236. const {state: storedState, pinnedFilters} = storedPageFilters;
  237. if (!hasProjectOrEnvironmentInUrl && pinnedFilters.has('projects')) {
  238. pageFilters.projects = storedState.project?.filter(validateProjectId) ?? [];
  239. if (pageFilters.projects.length < (storedState.project?.length ?? 0)) {
  240. shouldUpdateLocalStorage = true; // update storage to remove invalid projects
  241. shouldCheckDesyncedURLState = false;
  242. }
  243. }
  244. if (
  245. !skipLoadLastUsedEnvironment &&
  246. !hasProjectOrEnvironmentInUrl &&
  247. pinnedFilters.has('environments')
  248. ) {
  249. pageFilters.environments =
  250. storedState.environment?.filter(validateEnvironment) ?? [];
  251. if (pageFilters.environments.length < (storedState.environment?.length ?? 0)) {
  252. shouldUpdateLocalStorage = true; // update storage to remove invalid environments
  253. shouldCheckDesyncedURLState = false;
  254. }
  255. }
  256. if (!hasDatetimeInUrl && pinnedFilters.has('datetime')) {
  257. pageFilters.datetime = getDatetimeFromState(storedState);
  258. shouldUsePinnedDatetime = true;
  259. }
  260. }
  261. const {projects, environments: environment, datetime} = pageFilters;
  262. let newProject: number[] | null = null;
  263. let project = projects;
  264. // Skip enforcing a single project if `shouldForceProject` is true, since a
  265. // component is controlling what that project needs to be. This is true
  266. // regardless if user has access to multi projects
  267. if (shouldForceProject && forceProject) {
  268. newProject = [getProjectIdFromProject(forceProject)];
  269. } else if (shouldEnforceSingleProject && !shouldForceProject) {
  270. // If user does not have access to `global-views` (e.g. multi project
  271. // select) *and* there is no `project` URL parameter, then we update URL
  272. // params with:
  273. //
  274. // 1) the first project from the list of requested projects from URL params
  275. // 2) first project user is a member of from org
  276. //
  277. // Note this is intentionally skipped if `shouldForceProject == true` since
  278. // we want to initialize store and wait for the forced project
  279. //
  280. if (projects && projects.length > 0) {
  281. // If there is a list of projects from URL params, select first project
  282. // from that list
  283. newProject = typeof projects === 'string' ? [Number(projects)] : [projects[0]];
  284. } else {
  285. // When we have finished loading the organization into the props, i.e.
  286. // the organization slug is consistent with the URL param--Sentry will
  287. // get the first project from the organization that the user is a member
  288. // of.
  289. newProject = [...memberProjects].slice(0, 1).map(getProjectIdFromProject);
  290. }
  291. }
  292. if (newProject) {
  293. pageFilters.projects = newProject;
  294. project = newProject;
  295. }
  296. const pinnedFilters = organization.features.includes('new-page-filter')
  297. ? new Set<PinnedPageFilter>(['projects', 'environments', 'datetime'])
  298. : storedPageFilters?.pinnedFilters ?? new Set();
  299. PageFiltersStore.onInitializeUrlState(pageFilters, pinnedFilters, shouldPersist);
  300. if (shouldUpdateLocalStorage) {
  301. setPageFiltersStorage(organization.slug, new Set(['projects', 'environments']));
  302. }
  303. if (shouldCheckDesyncedURLState) {
  304. checkDesyncedUrlState(router, shouldForceProject);
  305. } else {
  306. // Clear desync state on route changes
  307. PageFiltersStore.updateDesyncedFilters(new Set());
  308. }
  309. const newDatetime = {
  310. ...datetime,
  311. period:
  312. parsed.start || parsed.end || parsed.period || shouldUsePinnedDatetime
  313. ? datetime.period
  314. : null,
  315. utc: parsed.utc || shouldUsePinnedDatetime ? datetime.utc : null,
  316. };
  317. if (!skipInitializeUrlParams) {
  318. updateParams({project, environment, ...newDatetime}, router, {
  319. replace: true,
  320. keepCursor: true,
  321. });
  322. }
  323. }
  324. function isProjectsValid(projects: number[]) {
  325. return Array.isArray(projects) && projects.every(isInteger);
  326. }
  327. /**
  328. * Updates store and selection URL param if `router` is supplied
  329. *
  330. * This accepts `environments` from `options` to also update environments
  331. * simultaneously as environments are tied to a project, so if you change
  332. * projects, you may need to clear environments.
  333. */
  334. export function updateProjects(
  335. projects: number[],
  336. router?: Router,
  337. options?: Options & {environments?: EnvironmentId[]}
  338. ) {
  339. if (!isProjectsValid(projects)) {
  340. Sentry.withScope(scope => {
  341. scope.setExtra('projects', projects);
  342. Sentry.captureException(new Error('Invalid projects selected'));
  343. });
  344. return;
  345. }
  346. PageFiltersStore.updateProjects(projects, options?.environments ?? null);
  347. updateParams({project: projects, environment: options?.environments}, router, options);
  348. persistPageFilters('projects', options);
  349. if (options?.environments) {
  350. persistPageFilters('environments', options);
  351. }
  352. }
  353. /**
  354. * Updates store and updates global environment selection URL param if `router` is supplied
  355. *
  356. * @param {String[]} environments List of environments
  357. * @param {Object} [router] Router object
  358. * @param {Object} [options] Options object
  359. * @param {String[]} [options.resetParams] List of parameters to remove when changing URL params
  360. */
  361. export function updateEnvironments(
  362. environment: EnvironmentId[] | null,
  363. router?: Router,
  364. options?: Options
  365. ) {
  366. PageFiltersStore.updateEnvironments(environment);
  367. updateParams({environment}, router, options);
  368. persistPageFilters('environments', options);
  369. }
  370. /**
  371. * Updates store and global datetime selection URL param if `router` is supplied
  372. *
  373. * @param {Object} datetime Object with start, end, range keys
  374. * @param {Object} [router] Router object
  375. * @param {Object} [options] Options object
  376. * @param {String[]} [options.resetParams] List of parameters to remove when changing URL params
  377. */
  378. export function updateDateTime(
  379. datetime: DateTimeUpdate,
  380. router?: Router,
  381. options?: Options
  382. ) {
  383. const {selection} = PageFiltersStore.getState();
  384. PageFiltersStore.updateDateTime({...selection.datetime, ...datetime});
  385. updateParams(datetime, router, options);
  386. persistPageFilters('datetime', options);
  387. }
  388. /**
  389. * Pins a particular filter so that it is read out of local storage
  390. */
  391. export function pinFilter(filter: PinnedPageFilter, pin: boolean) {
  392. PageFiltersStore.pin(filter, pin);
  393. persistPageFilters(null, {save: true});
  394. }
  395. /**
  396. * Changes whether any value updates will be persisted into local storage.
  397. */
  398. export function updatePersistence(shouldPersist: boolean) {
  399. PageFiltersStore.updatePersistence(shouldPersist);
  400. }
  401. /**
  402. * Updates router/URL with new query params
  403. *
  404. * @param obj New query params
  405. * @param [router] React router object
  406. * @param [options] Options object
  407. */
  408. function updateParams(obj: PageFiltersUpdate, router?: Router, options?: Options) {
  409. // Allow another component to handle routing
  410. if (!router) {
  411. return;
  412. }
  413. const newQuery = getNewQueryParams(obj, router.location.query, options);
  414. // Only push new location if query params has changed because this will cause a heavy re-render
  415. if (qs.stringify(newQuery) === qs.stringify(router.location.query)) {
  416. return;
  417. }
  418. const routerAction = options?.replace ? router.replace : router.push;
  419. routerAction({pathname: router.location.pathname, query: newQuery});
  420. }
  421. /**
  422. * Save a specific page filter to local storage.
  423. *
  424. * Pinned state is always persisted.
  425. */
  426. async function persistPageFilters(filter: PinnedPageFilter | null, options?: Options) {
  427. if (!options?.save || !PageFiltersStore.shouldPersist) {
  428. return;
  429. }
  430. // XXX(epurkhiser): Since this is called immediately after updating the
  431. // store, wait for a tick since stores are not updated fully synchronously.
  432. // A bit goofy, but it works fine.
  433. await new Promise(resolve => window.setTimeout(resolve, 0));
  434. const {organization} = OrganizationStore.getState();
  435. const orgSlug = organization?.slug ?? null;
  436. // Can't do anything if we don't have an organization
  437. if (orgSlug === null) {
  438. return;
  439. }
  440. const targetFilter = filter !== null ? [filter] : [];
  441. setPageFiltersStorage(
  442. orgSlug,
  443. new Set<PinnedPageFilter>(targetFilter),
  444. options.storageNamespace
  445. );
  446. }
  447. /**
  448. * Checks if the URL state has changed in synchronization from the local
  449. * storage state, and persists that check into the store.
  450. *
  451. * If shouldForceProject is enabled, then we do not record any url desync
  452. * for the project.
  453. */
  454. async function checkDesyncedUrlState(router?: Router, shouldForceProject?: boolean) {
  455. // Cannot compare URL state without the router
  456. if (!router || !PageFiltersStore.shouldPersist) {
  457. return;
  458. }
  459. const {query} = router.location;
  460. // XXX(epurkhiser): Since this is called immediately after updating the
  461. // store, wait for a tick since stores are not updated fully synchronously.
  462. // This function *should* be called only after persistPageFilters has been
  463. // called as well This function *should* be called only after
  464. // persistPageFilters has been called as well
  465. await new Promise(resolve => window.setTimeout(resolve, 0));
  466. const {organization} = OrganizationStore.getState();
  467. // Can't do anything if we don't have an organization
  468. if (organization === null) {
  469. return;
  470. }
  471. const storedPageFilters = getPageFilterStorage(organization.slug);
  472. // If we don't have any stored page filters then we do not check desynced state
  473. if (!storedPageFilters) {
  474. PageFiltersStore.updateDesyncedFilters(new Set<PinnedPageFilter>());
  475. return;
  476. }
  477. const currentQuery = getStateFromQuery(query, {
  478. allowAbsoluteDatetime: true,
  479. allowEmptyPeriod: true,
  480. });
  481. const differingFilters = new Set<PinnedPageFilter>();
  482. const {pinnedFilters, state: storedState} = storedPageFilters;
  483. // Are selected projects different?
  484. if (
  485. pinnedFilters.has('projects') &&
  486. currentQuery.project !== null &&
  487. !valueIsEqual(currentQuery.project, storedState.project) &&
  488. !shouldForceProject
  489. ) {
  490. differingFilters.add('projects');
  491. }
  492. // Are selected environments different?
  493. if (
  494. pinnedFilters.has('environments') &&
  495. currentQuery.environment !== null &&
  496. !valueIsEqual(currentQuery.environment, storedState.environment)
  497. ) {
  498. differingFilters.add('environments');
  499. }
  500. const dateTimeInQuery =
  501. currentQuery.end !== null ||
  502. currentQuery.start !== null ||
  503. currentQuery.utc !== null ||
  504. currentQuery.period !== null;
  505. // Is the datetime filter different?
  506. if (
  507. pinnedFilters.has('datetime') &&
  508. dateTimeInQuery &&
  509. (currentQuery.period !== storedState.period ||
  510. currentQuery.start?.getTime() !== storedState.start?.getTime() ||
  511. currentQuery.end?.getTime() !== storedState.end?.getTime() ||
  512. currentQuery.utc !== storedState.utc)
  513. ) {
  514. differingFilters.add('datetime');
  515. }
  516. PageFiltersStore.updateDesyncedFilters(differingFilters);
  517. }
  518. /**
  519. * Commits the new desynced filter values and clears the desynced filters list.
  520. */
  521. export function saveDesyncedFilters() {
  522. const {desyncedFilters} = PageFiltersStore;
  523. [...desyncedFilters].forEach(filter => persistPageFilters(filter, {save: true}));
  524. PageFiltersStore.updateDesyncedFilters(new Set());
  525. }
  526. /**
  527. * Merges an UpdateParams object into a Location['query'] object. Results in a
  528. * PageFilterQuery
  529. *
  530. * Preserves the old query params, except for `cursor` (can be overridden with
  531. * keepCursor option)
  532. *
  533. * @param obj New query params
  534. * @param currentQuery The current query parameters
  535. * @param [options] Options object
  536. */
  537. function getNewQueryParams(
  538. obj: PageFiltersUpdate,
  539. currentQuery: Location['query'],
  540. options: Options = {}
  541. ) {
  542. const {resetParams, keepCursor} = options;
  543. const cleanCurrentQuery = resetParams?.length
  544. ? omit(currentQuery, resetParams)
  545. : currentQuery;
  546. // Normalize existing query parameters
  547. const currentQueryState = getStateFromQuery(cleanCurrentQuery, {
  548. allowEmptyPeriod: true,
  549. allowAbsoluteDatetime: true,
  550. });
  551. // Extract non page filter parameters.
  552. const cursorParam = !keepCursor ? 'cursor' : null;
  553. const omittedParameters = [...Object.values(URL_PARAM), cursorParam].filter(defined);
  554. const extraParams = omit(cleanCurrentQuery, omittedParameters);
  555. // Override parameters
  556. const {project, environment, start, end, utc} = {
  557. ...currentQueryState,
  558. ...obj,
  559. };
  560. // Only set a stats period if we don't have an absolute date
  561. const statsPeriod = !start && !end ? obj.period || currentQueryState.period : null;
  562. const newQuery: PageFilterQuery = {
  563. project: project?.map(String),
  564. environment,
  565. start: statsPeriod ? null : start instanceof Date ? getUtcDateString(start) : start,
  566. end: statsPeriod ? null : end instanceof Date ? getUtcDateString(end) : end,
  567. utc: utc ? 'true' : null,
  568. statsPeriod,
  569. ...extraParams,
  570. };
  571. const paramEntries = Object.entries(newQuery).filter(([_, value]) => defined(value));
  572. return Object.fromEntries(paramEntries) as PageFilterQuery;
  573. }
  574. export function revertToPinnedFilters(orgSlug: string, router: InjectedRouter) {
  575. const {selection, desyncedFilters} = PageFiltersStore.getState();
  576. const storedFilterState = getPageFilterStorage(orgSlug)?.state;
  577. if (!storedFilterState) {
  578. return;
  579. }
  580. const newParams = {
  581. project: desyncedFilters.has('projects')
  582. ? storedFilterState.project
  583. : selection.projects,
  584. environment: desyncedFilters.has('environments')
  585. ? storedFilterState.environment
  586. : selection.environments,
  587. ...(desyncedFilters.has('datetime')
  588. ? pick(storedFilterState, DATE_TIME_KEYS)
  589. : selection.datetime),
  590. };
  591. updateParams(newParams, router, {
  592. keepCursor: true,
  593. });
  594. PageFiltersStore.updateDesyncedFilters(new Set());
  595. }