pageFilters.tsx 22 KB

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