eventView.tsx 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440
  1. import {Location, Query} from 'history';
  2. import cloneDeep from 'lodash/cloneDeep';
  3. import isEqual from 'lodash/isEqual';
  4. import isString from 'lodash/isString';
  5. import omit from 'lodash/omit';
  6. import pick from 'lodash/pick';
  7. import uniqBy from 'lodash/uniqBy';
  8. import moment from 'moment';
  9. import {EventQuery} from 'sentry/actionCreators/events';
  10. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  11. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  12. import {DEFAULT_PER_PAGE} from 'sentry/constants';
  13. import {URL_PARAM} from 'sentry/constants/pageFilters';
  14. import {t} from 'sentry/locale';
  15. import {NewQuery, PageFilters, SavedQuery, SelectValue, User} from 'sentry/types';
  16. import {
  17. aggregateOutputType,
  18. Column,
  19. ColumnType,
  20. Field,
  21. generateFieldAsString,
  22. getAggregateAlias,
  23. getEquation,
  24. isAggregateEquation,
  25. isAggregateField,
  26. isEquation,
  27. isLegalYAxisType,
  28. Sort,
  29. WebVital,
  30. } from 'sentry/utils/discover/fields';
  31. import {
  32. CHART_AXIS_OPTIONS,
  33. DISPLAY_MODE_FALLBACK_OPTIONS,
  34. DISPLAY_MODE_OPTIONS,
  35. DisplayModes,
  36. TOP_N,
  37. } from 'sentry/utils/discover/types';
  38. import {decodeList, decodeScalar} from 'sentry/utils/queryString';
  39. import {
  40. FieldValueKind,
  41. TableColumn,
  42. TableColumnSort,
  43. } from 'sentry/views/eventsV2/table/types';
  44. import {decodeColumnOrder} from 'sentry/views/eventsV2/utils';
  45. import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
  46. import {EventsDisplayFilterName} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
  47. import {statsPeriodToDays} from '../dates';
  48. import {MutableSearch} from '../tokenizeSearch';
  49. import {getSortField} from './fieldRenderers';
  50. // Metadata mapping for discover results.
  51. export type MetaType = Record<string, ColumnType> & {isMetricsData?: boolean};
  52. export type EventsMetaType = {fields: Record<string, ColumnType>} & {
  53. isMetricsData?: boolean;
  54. };
  55. // Data in discover results.
  56. export type EventData = Record<string, any>;
  57. export type LocationQuery = {
  58. cursor?: string | string[] | null;
  59. end?: string | string[] | null;
  60. start?: string | string[] | null;
  61. statsPeriod?: string | string[] | null;
  62. utc?: string | string[] | null;
  63. };
  64. const DATETIME_QUERY_STRING_KEYS = ['start', 'end', 'utc', 'statsPeriod'] as const;
  65. const EXTERNAL_QUERY_STRING_KEYS: Readonly<Array<keyof LocationQuery>> = [
  66. ...DATETIME_QUERY_STRING_KEYS,
  67. 'cursor',
  68. ];
  69. const setSortOrder = (sort: Sort, kind: 'desc' | 'asc'): Sort => ({
  70. kind,
  71. field: sort.field,
  72. });
  73. const reverseSort = (sort: Sort): Sort => ({
  74. kind: sort.kind === 'desc' ? 'asc' : 'desc',
  75. field: sort.field,
  76. });
  77. const isSortEqualToField = (
  78. sort: Sort,
  79. field: Field,
  80. tableMeta: MetaType | undefined
  81. ): boolean => {
  82. const sortKey = getSortKeyFromField(field, tableMeta);
  83. return sort.field === sortKey;
  84. };
  85. const fieldToSort = (
  86. field: Field,
  87. tableMeta: MetaType | undefined,
  88. kind?: 'desc' | 'asc',
  89. useFunctionFormat?: boolean
  90. ): Sort | undefined => {
  91. const sortKey = getSortKeyFromField(field, tableMeta, useFunctionFormat);
  92. if (!sortKey) {
  93. return void 0;
  94. }
  95. return {
  96. kind: kind || 'desc',
  97. field: sortKey,
  98. };
  99. };
  100. function getSortKeyFromField(
  101. field: Field,
  102. tableMeta?: MetaType,
  103. useFunctionFormat?: boolean
  104. ): string | null {
  105. const fieldString = useFunctionFormat ? field.field : getAggregateAlias(field.field);
  106. return getSortField(fieldString, tableMeta);
  107. }
  108. export function isFieldSortable(field: Field, tableMeta?: MetaType): boolean {
  109. return !!getSortKeyFromField(field, tableMeta);
  110. }
  111. const decodeFields = (location: Location): Array<Field> => {
  112. const {query} = location;
  113. if (!query || !query.field) {
  114. return [];
  115. }
  116. const fields = decodeList(query.field);
  117. const widths = decodeList(query.widths);
  118. const parsed: Field[] = [];
  119. fields.forEach((field, i) => {
  120. const w = Number(widths[i]);
  121. const width = !isNaN(w) ? w : COL_WIDTH_UNDEFINED;
  122. parsed.push({field, width});
  123. });
  124. return parsed;
  125. };
  126. const parseSort = (sort: string): Sort => {
  127. sort = sort.trim();
  128. if (sort.startsWith('-')) {
  129. return {
  130. kind: 'desc',
  131. field: sort.substring(1),
  132. };
  133. }
  134. return {
  135. kind: 'asc',
  136. field: sort,
  137. };
  138. };
  139. export const fromSorts = (sorts: string | string[] | undefined): Array<Sort> => {
  140. if (sorts === undefined) {
  141. return [];
  142. }
  143. sorts = isString(sorts) ? [sorts] : sorts;
  144. // NOTE: sets are iterated in insertion order
  145. const uniqueSorts = [...new Set(sorts)];
  146. return uniqueSorts.reduce((acc: Array<Sort>, sort: string) => {
  147. acc.push(parseSort(sort));
  148. return acc;
  149. }, []);
  150. };
  151. const decodeSorts = (location: Location): Array<Sort> => {
  152. const {query} = location;
  153. if (!query || !query.sort) {
  154. return [];
  155. }
  156. const sorts = decodeList(query.sort);
  157. return fromSorts(sorts);
  158. };
  159. export const encodeSort = (sort: Sort): string => {
  160. switch (sort.kind) {
  161. case 'desc': {
  162. return `-${sort.field}`;
  163. }
  164. case 'asc': {
  165. return String(sort.field);
  166. }
  167. default: {
  168. throw new Error('Unexpected sort type');
  169. }
  170. }
  171. };
  172. const encodeSorts = (sorts: Readonly<Array<Sort>>): Array<string> =>
  173. sorts.map(encodeSort);
  174. const collectQueryStringByKey = (query: Query, key: string): Array<string> => {
  175. const needle = query[key];
  176. const collection = decodeList(needle);
  177. return collection.reduce((acc: Array<string>, item: string) => {
  178. item = item.trim();
  179. if (item.length > 0) {
  180. acc.push(item);
  181. }
  182. return acc;
  183. }, []);
  184. };
  185. const decodeQuery = (location: Location): string => {
  186. if (!location.query || !location.query.query) {
  187. return '';
  188. }
  189. const queryParameter = location.query.query;
  190. return decodeScalar(queryParameter, '').trim();
  191. };
  192. const decodeTeam = (value: string): 'myteams' | number => {
  193. if (value === 'myteams') {
  194. return value;
  195. }
  196. return parseInt(value, 10);
  197. };
  198. const decodeTeams = (location: Location): ('myteams' | number)[] => {
  199. if (!location.query || !location.query.team) {
  200. return [];
  201. }
  202. const value = location.query.team;
  203. return (Array.isArray(value) ? value.map(decodeTeam) : [decodeTeam(value)]).filter(
  204. team => team === 'myteams' || !isNaN(team)
  205. );
  206. };
  207. const decodeProjects = (location: Location): number[] => {
  208. if (!location.query || !location.query.project) {
  209. return [];
  210. }
  211. const value = location.query.project;
  212. return Array.isArray(value) ? value.map(i => parseInt(i, 10)) : [parseInt(value, 10)];
  213. };
  214. const queryStringFromSavedQuery = (saved: NewQuery | SavedQuery): string => {
  215. if (saved.query) {
  216. return saved.query || '';
  217. }
  218. return '';
  219. };
  220. function validateTableMeta(tableMeta: MetaType | undefined): MetaType | undefined {
  221. return tableMeta && Object.keys(tableMeta).length > 0 ? tableMeta : undefined;
  222. }
  223. class EventView {
  224. id: string | undefined;
  225. name: string | undefined;
  226. fields: Readonly<Field[]>;
  227. sorts: Readonly<Sort[]>;
  228. query: string;
  229. team: Readonly<('myteams' | number)[]>;
  230. project: Readonly<number[]>;
  231. start: string | undefined;
  232. end: string | undefined;
  233. statsPeriod: string | undefined;
  234. utc?: string | boolean | undefined;
  235. environment: Readonly<string[]>;
  236. yAxis: string | undefined;
  237. display: string | undefined;
  238. topEvents: string | undefined;
  239. interval: string | undefined;
  240. expired?: boolean;
  241. createdBy: User | undefined;
  242. additionalConditions: MutableSearch; // This allows views to always add additional conditions to the query to get specific data. It should not show up in the UI unless explicitly called.
  243. constructor(props: {
  244. additionalConditions: MutableSearch;
  245. createdBy: User | undefined;
  246. display: string | undefined;
  247. end: string | undefined;
  248. environment: Readonly<string[]>;
  249. fields: Readonly<Field[]>;
  250. id: string | undefined;
  251. name: string | undefined;
  252. project: Readonly<number[]>;
  253. query: string;
  254. sorts: Readonly<Sort[]>;
  255. start: string | undefined;
  256. statsPeriod: string | undefined;
  257. team: Readonly<('myteams' | number)[]>;
  258. topEvents: string | undefined;
  259. yAxis: string | undefined;
  260. expired?: boolean;
  261. interval?: string;
  262. utc?: string | boolean | undefined;
  263. }) {
  264. const fields: Field[] = Array.isArray(props.fields) ? props.fields : [];
  265. let sorts: Sort[] = Array.isArray(props.sorts) ? props.sorts : [];
  266. const team = Array.isArray(props.team) ? props.team : [];
  267. const project = Array.isArray(props.project) ? props.project : [];
  268. const environment = Array.isArray(props.environment) ? props.environment : [];
  269. // only include sort keys that are included in the fields
  270. let equations = 0;
  271. const sortKeys = fields
  272. .map(field => {
  273. if (field.field && isEquation(field.field)) {
  274. const sortKey = getSortKeyFromField(
  275. {field: `equation[${equations}]`},
  276. undefined
  277. );
  278. equations += 1;
  279. return sortKey;
  280. }
  281. return getSortKeyFromField(field, undefined);
  282. })
  283. .filter((sortKey): sortKey is string => !!sortKey);
  284. const sort = sorts.find(currentSort => sortKeys.includes(currentSort.field));
  285. sorts = sort ? [sort] : [];
  286. const id = props.id !== null && props.id !== void 0 ? String(props.id) : void 0;
  287. this.id = id;
  288. this.name = props.name;
  289. this.fields = fields;
  290. this.sorts = sorts;
  291. this.query = typeof props.query === 'string' ? props.query : '';
  292. this.team = team;
  293. this.project = project;
  294. this.start = props.start;
  295. this.end = props.end;
  296. this.statsPeriod = props.statsPeriod;
  297. this.utc = props.utc;
  298. this.environment = environment;
  299. this.yAxis = props.yAxis;
  300. this.display = props.display;
  301. this.topEvents = props.topEvents;
  302. this.interval = props.interval;
  303. this.createdBy = props.createdBy;
  304. this.expired = props.expired;
  305. this.additionalConditions = props.additionalConditions
  306. ? props.additionalConditions.copy()
  307. : new MutableSearch([]);
  308. }
  309. static fromLocation(location: Location): EventView {
  310. const {start, end, statsPeriod} = normalizeDateTimeParams(location.query);
  311. return new EventView({
  312. id: decodeScalar(location.query.id),
  313. name: decodeScalar(location.query.name),
  314. fields: decodeFields(location),
  315. sorts: decodeSorts(location),
  316. query: decodeQuery(location),
  317. team: decodeTeams(location),
  318. project: decodeProjects(location),
  319. start: decodeScalar(start),
  320. end: decodeScalar(end),
  321. statsPeriod: decodeScalar(statsPeriod),
  322. environment: collectQueryStringByKey(location.query, 'environment'),
  323. yAxis: decodeScalar(location.query.yAxis),
  324. display: decodeScalar(location.query.display),
  325. topEvents: decodeScalar(location.query.topEvents),
  326. interval: decodeScalar(location.query.interval),
  327. createdBy: undefined,
  328. additionalConditions: new MutableSearch([]),
  329. });
  330. }
  331. static fromNewQueryWithLocation(newQuery: NewQuery, location: Location): EventView {
  332. const query = location.query;
  333. // apply global selection header values from location whenever possible
  334. const environment: string[] =
  335. Array.isArray(newQuery.environment) && newQuery.environment.length > 0
  336. ? newQuery.environment
  337. : collectQueryStringByKey(query, 'environment');
  338. const project: number[] =
  339. Array.isArray(newQuery.projects) && newQuery.projects.length > 0
  340. ? newQuery.projects
  341. : decodeProjects(location);
  342. const saved: NewQuery = {
  343. ...newQuery,
  344. environment,
  345. projects: project,
  346. // datetime selection
  347. start: newQuery.start || decodeScalar(query.start),
  348. end: newQuery.end || decodeScalar(query.end),
  349. range: newQuery.range || decodeScalar(query.statsPeriod),
  350. };
  351. return EventView.fromSavedQuery(saved);
  352. }
  353. static getFields(saved: NewQuery | SavedQuery) {
  354. return saved.fields.map((field, i) => {
  355. const width =
  356. saved.widths && saved.widths[i] ? Number(saved.widths[i]) : COL_WIDTH_UNDEFINED;
  357. return {field, width};
  358. });
  359. }
  360. static fromSavedQuery(saved: NewQuery | SavedQuery): EventView {
  361. const fields = EventView.getFields(saved);
  362. // normalize datetime selection
  363. const {start, end, statsPeriod, utc} = normalizeDateTimeParams({
  364. start: saved.start,
  365. end: saved.end,
  366. statsPeriod: saved.range,
  367. utc: saved.utc,
  368. });
  369. return new EventView({
  370. id: saved.id,
  371. name: saved.name,
  372. fields,
  373. query: queryStringFromSavedQuery(saved),
  374. team: saved.teams ?? [],
  375. project: saved.projects,
  376. start: decodeScalar(start),
  377. end: decodeScalar(end),
  378. statsPeriod: decodeScalar(statsPeriod),
  379. utc,
  380. sorts: fromSorts(saved.orderby),
  381. environment: collectQueryStringByKey(
  382. {
  383. environment: saved.environment as string[],
  384. },
  385. 'environment'
  386. ),
  387. // Workaround to only use the first yAxis since eventView yAxis doesn't accept string[]
  388. yAxis: Array.isArray(saved.yAxis) ? saved.yAxis[0] : saved.yAxis,
  389. display: saved.display,
  390. topEvents: saved.topEvents ? saved.topEvents.toString() : undefined,
  391. createdBy: saved.createdBy,
  392. expired: saved.expired,
  393. additionalConditions: new MutableSearch([]),
  394. });
  395. }
  396. static fromSavedQueryOrLocation(
  397. saved: SavedQuery | undefined,
  398. location: Location
  399. ): EventView {
  400. let fields = decodeFields(location);
  401. const {start, end, statsPeriod, utc} = normalizeDateTimeParams(location.query);
  402. const id = decodeScalar(location.query.id);
  403. const teams = decodeTeams(location);
  404. const projects = decodeProjects(location);
  405. const sorts = decodeSorts(location);
  406. const environments = collectQueryStringByKey(location.query, 'environment');
  407. if (saved) {
  408. if (fields.length === 0) {
  409. fields = EventView.getFields(saved);
  410. }
  411. return new EventView({
  412. id: id || saved.id,
  413. name: decodeScalar(location.query.name) || saved.name,
  414. fields,
  415. query:
  416. 'query' in location.query
  417. ? decodeQuery(location)
  418. : queryStringFromSavedQuery(saved),
  419. sorts: sorts.length === 0 ? fromSorts(saved.orderby) : sorts,
  420. yAxis:
  421. decodeScalar(location.query.yAxis) ||
  422. // Workaround to only use the first yAxis since eventView yAxis doesn't accept string[]
  423. (Array.isArray(saved.yAxis) ? saved.yAxis[0] : saved.yAxis),
  424. display: decodeScalar(location.query.display) || saved.display,
  425. topEvents: (
  426. decodeScalar(location.query.topEvents) ||
  427. saved.topEvents ||
  428. TOP_N
  429. ).toString(),
  430. interval: decodeScalar(location.query.interval),
  431. createdBy: saved.createdBy,
  432. expired: saved.expired,
  433. additionalConditions: new MutableSearch([]),
  434. // Always read team from location since they can be set by other parts
  435. // of the UI
  436. team: teams,
  437. // Always read project and environment from location since they can
  438. // be set by the GlobalSelectionHeaders.
  439. project: projects,
  440. environment: environments,
  441. start: decodeScalar(start),
  442. end: decodeScalar(end),
  443. statsPeriod: decodeScalar(statsPeriod),
  444. utc,
  445. });
  446. }
  447. return EventView.fromLocation(location);
  448. }
  449. isEqualTo(other: EventView): boolean {
  450. const defaults = {
  451. id: undefined,
  452. name: undefined,
  453. query: undefined,
  454. statsPeriod: undefined,
  455. fields: undefined,
  456. sorts: undefined,
  457. project: undefined,
  458. environment: undefined,
  459. yAxis: 'count()',
  460. display: DisplayModes.DEFAULT,
  461. topEvents: '5',
  462. };
  463. const keys = Object.keys(defaults);
  464. for (const key of keys) {
  465. const currentValue = this[key] ?? defaults[key];
  466. const otherValue = other[key] ?? defaults[key];
  467. if (!isEqual(currentValue, otherValue)) {
  468. return false;
  469. }
  470. }
  471. // compare datetime selections using moment
  472. const dateTimeKeys = ['start', 'end'];
  473. for (const key of dateTimeKeys) {
  474. const currentValue = this[key];
  475. const otherValue = other[key];
  476. if (currentValue && otherValue) {
  477. const currentDateTime = moment.utc(currentValue);
  478. const otherDateTime = moment.utc(otherValue);
  479. if (!currentDateTime.isSame(otherDateTime)) {
  480. return false;
  481. }
  482. }
  483. }
  484. return true;
  485. }
  486. toNewQuery(): NewQuery {
  487. const orderby = this.sorts.length > 0 ? encodeSorts(this.sorts)[0] : undefined;
  488. const newQuery: NewQuery = {
  489. version: 2,
  490. id: this.id,
  491. name: this.name || '',
  492. fields: this.getFields(),
  493. widths: this.getWidths().map(w => String(w)),
  494. orderby,
  495. query: this.query || '',
  496. projects: this.project,
  497. start: this.start,
  498. end: this.end,
  499. range: this.statsPeriod,
  500. environment: this.environment,
  501. yAxis: this.yAxis ? [this.yAxis] : undefined,
  502. display: this.display,
  503. topEvents: this.topEvents,
  504. };
  505. if (!newQuery.query) {
  506. // if query is an empty string, then it cannot be saved, so we omit it
  507. // from the payload
  508. delete newQuery.query;
  509. }
  510. return newQuery;
  511. }
  512. getPageFilters(): PageFilters {
  513. return {
  514. projects: this.project as number[],
  515. environments: this.environment as string[],
  516. datetime: {
  517. start: this.start ?? null,
  518. end: this.end ?? null,
  519. period: this.statsPeriod ?? null,
  520. // TODO(tony) Add support for the Use UTC option from the global
  521. // headers, currently, that option is not supported and all times are
  522. // assumed to be UTC
  523. utc: true,
  524. },
  525. };
  526. }
  527. getPageFiltersQuery(): Query {
  528. const {
  529. environments: environment,
  530. projects,
  531. datetime: {start, end, period, utc},
  532. } = this.getPageFilters();
  533. return {
  534. project: projects.map(proj => proj.toString()),
  535. environment,
  536. utc: utc ? 'true' : 'false',
  537. // since these values are from `getGlobalSelection`
  538. // we know they have type `string | null`
  539. start: (start ?? undefined) as string | undefined,
  540. end: (end ?? undefined) as string | undefined,
  541. // we can't use the ?? operator here as we want to
  542. // convert the empty string to undefined
  543. statsPeriod: period ? period : undefined,
  544. };
  545. }
  546. generateBlankQueryStringObject(): Query {
  547. const output = {
  548. id: undefined,
  549. name: undefined,
  550. field: undefined,
  551. widths: undefined,
  552. sort: undefined,
  553. tag: undefined,
  554. query: undefined,
  555. yAxis: undefined,
  556. display: undefined,
  557. topEvents: undefined,
  558. interval: undefined,
  559. };
  560. for (const field of EXTERNAL_QUERY_STRING_KEYS) {
  561. output[field] = undefined;
  562. }
  563. return output;
  564. }
  565. generateQueryStringObject(): Query {
  566. const output = {
  567. id: this.id,
  568. name: this.name,
  569. field: this.getFields(),
  570. widths: this.getWidths(),
  571. sort: encodeSorts(this.sorts),
  572. environment: this.environment,
  573. project: this.project,
  574. query: this.query,
  575. yAxis: this.yAxis || this.getYAxis(),
  576. display: this.display,
  577. topEvents: this.topEvents,
  578. interval: this.interval,
  579. };
  580. for (const field of EXTERNAL_QUERY_STRING_KEYS) {
  581. if (this[field] && this[field].length) {
  582. output[field] = this[field];
  583. }
  584. }
  585. return cloneDeep(output as any);
  586. }
  587. isValid(): boolean {
  588. return this.fields.length > 0;
  589. }
  590. getWidths(): number[] {
  591. const result = this.fields.map(field =>
  592. field.width ? field.width : COL_WIDTH_UNDEFINED
  593. );
  594. while (result.length > 0) {
  595. const width = result[result.length - 1];
  596. if (width === COL_WIDTH_UNDEFINED) {
  597. result.pop();
  598. continue;
  599. }
  600. break;
  601. }
  602. return result;
  603. }
  604. getFields(): string[] {
  605. return this.fields.map(field => field.field);
  606. }
  607. getEquations(): string[] {
  608. return this.fields
  609. .filter(field => isEquation(field.field))
  610. .map(field => getEquation(field.field));
  611. }
  612. getAggregateFields(): Field[] {
  613. return this.fields.filter(
  614. field => isAggregateField(field.field) || isAggregateEquation(field.field)
  615. );
  616. }
  617. hasAggregateField() {
  618. return this.fields.some(field => isAggregateField(field.field));
  619. }
  620. hasIdField() {
  621. return this.fields.some(field => field.field === 'id');
  622. }
  623. numOfColumns(): number {
  624. return this.fields.length;
  625. }
  626. getColumns(useFullEquationAsKey?: boolean): TableColumn<React.ReactText>[] {
  627. return decodeColumnOrder(this.fields, useFullEquationAsKey);
  628. }
  629. getDays(): number {
  630. const statsPeriod = decodeScalar(this.statsPeriod);
  631. return statsPeriodToDays(statsPeriod, this.start, this.end);
  632. }
  633. clone(): EventView {
  634. // NOTE: We rely on usage of Readonly from TypeScript to ensure we do not mutate
  635. // the attributes of EventView directly. This enables us to quickly
  636. // clone new instances of EventView.
  637. return new EventView({
  638. id: this.id,
  639. name: this.name,
  640. fields: this.fields,
  641. sorts: this.sorts,
  642. query: this.query,
  643. team: this.team,
  644. project: this.project,
  645. start: this.start,
  646. end: this.end,
  647. statsPeriod: this.statsPeriod,
  648. environment: this.environment,
  649. yAxis: this.yAxis,
  650. display: this.display,
  651. topEvents: this.topEvents,
  652. interval: this.interval,
  653. expired: this.expired,
  654. createdBy: this.createdBy,
  655. additionalConditions: this.additionalConditions.copy(),
  656. });
  657. }
  658. withSorts(sorts: Sort[]): EventView {
  659. const newEventView = this.clone();
  660. const fields = newEventView.fields.map(field => getAggregateAlias(field.field));
  661. newEventView.sorts = sorts.filter(sort => fields.includes(sort.field));
  662. return newEventView;
  663. }
  664. withColumns(columns: Column[]): EventView {
  665. const newEventView = this.clone();
  666. const fields: Field[] = columns
  667. .filter(
  668. col =>
  669. ((col.kind === 'field' || col.kind === FieldValueKind.EQUATION) && col.field) ||
  670. (col.kind === 'function' && col.function[0])
  671. )
  672. .map(col => generateFieldAsString(col))
  673. .map((field, i) => {
  674. // newly added field
  675. if (!newEventView.fields[i]) {
  676. return {field, width: COL_WIDTH_UNDEFINED};
  677. }
  678. // Existing columns that were not re ordered should retain
  679. // their old widths.
  680. const existing = newEventView.fields[i];
  681. const width =
  682. existing.field === field && existing.width !== undefined
  683. ? existing.width
  684. : COL_WIDTH_UNDEFINED;
  685. return {field, width};
  686. });
  687. newEventView.fields = fields;
  688. // Update sorts as sorted fields may have been removed.
  689. if (newEventView.sorts) {
  690. // Filter the sort fields down to those that are still selected.
  691. const sortKeys = fields.map(field => fieldToSort(field, undefined)?.field);
  692. const newSort = newEventView.sorts.filter(
  693. sort => sort && sortKeys.includes(sort.field)
  694. );
  695. // If the sort field was removed, try and find a new sortable column.
  696. if (newSort.length === 0) {
  697. const sortField = fields.find(field => isFieldSortable(field, undefined));
  698. if (sortField) {
  699. newSort.push({field: sortField.field, kind: 'desc'});
  700. }
  701. }
  702. newEventView.sorts = newSort;
  703. }
  704. newEventView.yAxis = newEventView.getYAxis();
  705. return newEventView;
  706. }
  707. withNewColumn(newColumn: Column): EventView {
  708. const fieldAsString = generateFieldAsString(newColumn);
  709. const newField: Field = {
  710. field: fieldAsString,
  711. width: COL_WIDTH_UNDEFINED,
  712. };
  713. const newEventView = this.clone();
  714. newEventView.fields = [...newEventView.fields, newField];
  715. return newEventView;
  716. }
  717. withResizedColumn(columnIndex: number, newWidth: number) {
  718. const field = this.fields[columnIndex];
  719. const newEventView = this.clone();
  720. if (!field) {
  721. return newEventView;
  722. }
  723. const updateWidth = field.width !== newWidth;
  724. if (updateWidth) {
  725. const fields = [...newEventView.fields];
  726. fields[columnIndex] = {
  727. ...field,
  728. width: newWidth,
  729. };
  730. newEventView.fields = fields;
  731. }
  732. return newEventView;
  733. }
  734. withUpdatedColumn(
  735. columnIndex: number,
  736. updatedColumn: Column,
  737. tableMeta: MetaType | undefined
  738. ): EventView {
  739. const columnToBeUpdated = this.fields[columnIndex];
  740. const fieldAsString = generateFieldAsString(updatedColumn);
  741. const updateField = columnToBeUpdated.field !== fieldAsString;
  742. if (!updateField) {
  743. return this;
  744. }
  745. // ensure tableMeta is non-empty
  746. tableMeta = validateTableMeta(tableMeta);
  747. const newEventView = this.clone();
  748. const updatedField: Field = {
  749. field: fieldAsString,
  750. width: COL_WIDTH_UNDEFINED,
  751. };
  752. const fields = [...newEventView.fields];
  753. fields[columnIndex] = updatedField;
  754. newEventView.fields = fields;
  755. // if the updated column is one of the sorted columns, we may need to remove
  756. // it from the list of sorts
  757. const needleSortIndex = this.sorts.findIndex(sort =>
  758. isSortEqualToField(sort, columnToBeUpdated, tableMeta)
  759. );
  760. if (needleSortIndex >= 0) {
  761. const needleSort = this.sorts[needleSortIndex];
  762. const numOfColumns = this.fields.reduce((sum, currentField) => {
  763. if (isSortEqualToField(needleSort, currentField, tableMeta)) {
  764. return sum + 1;
  765. }
  766. return sum;
  767. }, 0);
  768. // do not bother deleting the sort key if there are more than one columns
  769. // of it in the table.
  770. if (numOfColumns <= 1) {
  771. if (isFieldSortable(updatedField, tableMeta)) {
  772. // use the current updated field as the sort key
  773. const sort = fieldToSort(updatedField, tableMeta)!;
  774. // preserve the sort kind
  775. sort.kind = needleSort.kind;
  776. const sorts = [...newEventView.sorts];
  777. sorts[needleSortIndex] = sort;
  778. newEventView.sorts = sorts;
  779. } else {
  780. const sorts = [...newEventView.sorts];
  781. sorts.splice(needleSortIndex, 1);
  782. newEventView.sorts = [...new Set(sorts)];
  783. }
  784. }
  785. if (newEventView.sorts.length <= 0 && newEventView.fields.length > 0) {
  786. // establish a default sort by finding the first sortable field
  787. if (isFieldSortable(updatedField, tableMeta)) {
  788. // use the current updated field as the sort key
  789. const sort = fieldToSort(updatedField, tableMeta)!;
  790. // preserve the sort kind
  791. sort.kind = needleSort.kind;
  792. newEventView.sorts = [sort];
  793. } else {
  794. const sortableFieldIndex = newEventView.fields.findIndex(currentField =>
  795. isFieldSortable(currentField, tableMeta)
  796. );
  797. if (sortableFieldIndex >= 0) {
  798. const fieldToBeSorted = newEventView.fields[sortableFieldIndex];
  799. const sort = fieldToSort(fieldToBeSorted, tableMeta)!;
  800. newEventView.sorts = [sort];
  801. }
  802. }
  803. }
  804. }
  805. newEventView.yAxis = newEventView.getYAxis();
  806. return newEventView;
  807. }
  808. withDeletedColumn(columnIndex: number, tableMeta: MetaType | undefined): EventView {
  809. // Disallow removal of the orphan column, and check for out-of-bounds
  810. if (this.fields.length <= 1 || this.fields.length <= columnIndex || columnIndex < 0) {
  811. return this;
  812. }
  813. // ensure tableMeta is non-empty
  814. tableMeta = validateTableMeta(tableMeta);
  815. // delete the column
  816. const newEventView = this.clone();
  817. const fields = [...newEventView.fields];
  818. fields.splice(columnIndex, 1);
  819. newEventView.fields = fields;
  820. // Ensure there is at least one auto width column
  821. // To ensure a well formed table results.
  822. const hasAutoIndex = fields.find(field => field.width === COL_WIDTH_UNDEFINED);
  823. if (!hasAutoIndex) {
  824. newEventView.fields[0].width = COL_WIDTH_UNDEFINED;
  825. }
  826. // if the deleted column is one of the sorted columns, we need to remove
  827. // it from the list of sorts
  828. const columnToBeDeleted = this.fields[columnIndex];
  829. const needleSortIndex = this.sorts.findIndex(sort =>
  830. isSortEqualToField(sort, columnToBeDeleted, tableMeta)
  831. );
  832. if (needleSortIndex >= 0) {
  833. const needleSort = this.sorts[needleSortIndex];
  834. const numOfColumns = this.fields.reduce((sum, field) => {
  835. if (isSortEqualToField(needleSort, field, tableMeta)) {
  836. return sum + 1;
  837. }
  838. return sum;
  839. }, 0);
  840. // do not bother deleting the sort key if there are more than one columns
  841. // of it in the table.
  842. if (numOfColumns <= 1) {
  843. const sorts = [...newEventView.sorts];
  844. sorts.splice(needleSortIndex, 1);
  845. newEventView.sorts = [...new Set(sorts)];
  846. if (newEventView.sorts.length <= 0 && newEventView.fields.length > 0) {
  847. // establish a default sort by finding the first sortable field
  848. const sortableFieldIndex = newEventView.fields.findIndex(field =>
  849. isFieldSortable(field, tableMeta)
  850. );
  851. if (sortableFieldIndex >= 0) {
  852. const fieldToBeSorted = newEventView.fields[sortableFieldIndex];
  853. const sort = fieldToSort(fieldToBeSorted, tableMeta)!;
  854. newEventView.sorts = [sort];
  855. }
  856. }
  857. }
  858. }
  859. newEventView.yAxis = newEventView.getYAxis();
  860. return newEventView;
  861. }
  862. withTeams(teams: ('myteams' | number)[]): EventView {
  863. const newEventView = this.clone();
  864. newEventView.team = teams;
  865. return newEventView;
  866. }
  867. getSorts(): TableColumnSort<React.ReactText>[] {
  868. return this.sorts.map(
  869. sort =>
  870. ({
  871. key: sort.field,
  872. order: sort.kind,
  873. } as TableColumnSort<string>)
  874. );
  875. }
  876. // returns query input for the search
  877. getQuery(inputQuery: string | string[] | null | undefined): string {
  878. const queryParts: string[] = [];
  879. if (this.query) {
  880. if (this.additionalConditions) {
  881. queryParts.push(this.getQueryWithAdditionalConditions());
  882. } else {
  883. queryParts.push(this.query);
  884. }
  885. }
  886. if (inputQuery) {
  887. // there may be duplicate query in the query string
  888. // e.g. query=hello&query=world
  889. if (Array.isArray(inputQuery)) {
  890. inputQuery.forEach(query => {
  891. if (typeof query === 'string' && !queryParts.includes(query)) {
  892. queryParts.push(query);
  893. }
  894. });
  895. }
  896. if (typeof inputQuery === 'string' && !queryParts.includes(inputQuery)) {
  897. queryParts.push(inputQuery);
  898. }
  899. }
  900. return queryParts.join(' ');
  901. }
  902. getFacetsAPIPayload(
  903. location: Location
  904. ): Exclude<EventQuery & LocationQuery, 'sort' | 'cursor'> {
  905. const payload = this.getEventsAPIPayload(location);
  906. const remove = [
  907. 'id',
  908. 'name',
  909. 'per_page',
  910. 'sort',
  911. 'cursor',
  912. 'field',
  913. 'equation',
  914. 'interval',
  915. ];
  916. for (const key of remove) {
  917. delete payload[key];
  918. }
  919. return payload;
  920. }
  921. normalizeDateSelection(location: Location) {
  922. const query = (location && location.query) || {};
  923. // pick only the query strings that we care about
  924. const picked = pickRelevantLocationQueryStrings(location);
  925. const hasDateSelection = this.statsPeriod || (this.start && this.end);
  926. // an eventview's date selection has higher precedence than the date selection in the query string
  927. const dateSelection = hasDateSelection
  928. ? {
  929. start: this.start,
  930. end: this.end,
  931. statsPeriod: this.statsPeriod,
  932. }
  933. : {
  934. start: picked.start,
  935. end: picked.end,
  936. period: decodeScalar(query.period),
  937. statsPeriod: picked.statsPeriod,
  938. };
  939. // normalize datetime selection
  940. return normalizeDateTimeParams({
  941. ...dateSelection,
  942. utc: decodeScalar(query.utc),
  943. });
  944. }
  945. // Takes an EventView instance and converts it into the format required for the events API
  946. getEventsAPIPayload(location: Location): EventQuery & LocationQuery {
  947. // pick only the query strings that we care about
  948. const picked = pickRelevantLocationQueryStrings(location);
  949. // normalize datetime selection
  950. const normalizedTimeWindowParams = this.normalizeDateSelection(location);
  951. const sort =
  952. this.sorts.length <= 0
  953. ? undefined
  954. : this.sorts.length > 1
  955. ? encodeSorts(this.sorts)
  956. : encodeSort(this.sorts[0]);
  957. const fields = this.getFields();
  958. const team = this.team.map(proj => String(proj));
  959. const project = this.project.map(proj => String(proj));
  960. const environment = this.environment as string[];
  961. // generate event query
  962. const eventQuery = Object.assign(
  963. omit(picked, DATETIME_QUERY_STRING_KEYS),
  964. normalizedTimeWindowParams,
  965. {
  966. team,
  967. project,
  968. environment,
  969. field: [...new Set(fields)],
  970. sort,
  971. per_page: DEFAULT_PER_PAGE,
  972. query: this.getQueryWithAdditionalConditions(),
  973. }
  974. ) as EventQuery & LocationQuery;
  975. if (eventQuery.team && !eventQuery.team.length) {
  976. delete eventQuery.team;
  977. }
  978. if (!eventQuery.sort) {
  979. delete eventQuery.sort;
  980. }
  981. return eventQuery;
  982. }
  983. getResultsViewUrlTarget(slug: string): {pathname: string; query: Query} {
  984. return {
  985. pathname: `/organizations/${slug}/discover/results/`,
  986. query: this.generateQueryStringObject(),
  987. };
  988. }
  989. getResultsViewShortUrlTarget(slug: string): {pathname: string; query: Query} {
  990. const output = {id: this.id};
  991. for (const field of [...Object.values(URL_PARAM), 'cursor']) {
  992. if (this[field] && this[field].length) {
  993. output[field] = this[field];
  994. }
  995. }
  996. return {
  997. pathname: `/organizations/${slug}/discover/results/`,
  998. query: cloneDeep(output as any),
  999. };
  1000. }
  1001. getPerformanceTransactionEventsViewUrlTarget(
  1002. slug: string,
  1003. options: {
  1004. breakdown?: SpanOperationBreakdownFilter;
  1005. showTransactions?: EventsDisplayFilterName;
  1006. webVital?: WebVital;
  1007. }
  1008. ): {pathname: string; query: Query} {
  1009. const {showTransactions, breakdown, webVital} = options;
  1010. const output = {
  1011. sort: encodeSorts(this.sorts),
  1012. project: this.project,
  1013. query: this.query,
  1014. transaction: this.name,
  1015. showTransactions,
  1016. breakdown,
  1017. webVital,
  1018. };
  1019. for (const field of EXTERNAL_QUERY_STRING_KEYS) {
  1020. if (this[field] && this[field].length) {
  1021. output[field] = this[field];
  1022. }
  1023. }
  1024. const query = cloneDeep(output as any);
  1025. return {
  1026. pathname: `/organizations/${slug}/performance/summary/events/`,
  1027. query,
  1028. };
  1029. }
  1030. sortForField(field: Field, tableMeta: MetaType | undefined): Sort | undefined {
  1031. if (!tableMeta) {
  1032. return undefined;
  1033. }
  1034. return this.sorts.find(sort => isSortEqualToField(sort, field, tableMeta));
  1035. }
  1036. sortOnField(
  1037. field: Field,
  1038. tableMeta: MetaType,
  1039. kind?: 'desc' | 'asc',
  1040. useFunctionFormat?: boolean
  1041. ): EventView {
  1042. // check if field can be sorted
  1043. if (!isFieldSortable(field, tableMeta)) {
  1044. return this;
  1045. }
  1046. const needleIndex = this.sorts.findIndex(sort =>
  1047. isSortEqualToField(sort, field, tableMeta)
  1048. );
  1049. if (needleIndex >= 0) {
  1050. const newEventView = this.clone();
  1051. const currentSort = this.sorts[needleIndex];
  1052. const sorts = [...newEventView.sorts];
  1053. sorts[needleIndex] = kind
  1054. ? setSortOrder(
  1055. {...currentSort, ...(useFunctionFormat ? {field: field.field} : {})},
  1056. kind
  1057. )
  1058. : reverseSort({
  1059. ...currentSort,
  1060. ...(useFunctionFormat ? {field: field.field} : {}),
  1061. });
  1062. newEventView.sorts = sorts;
  1063. return newEventView;
  1064. }
  1065. // field is currently not sorted; so, we sort on it
  1066. const newEventView = this.clone();
  1067. // invariant: this is not falsey, since sortKey exists
  1068. const sort = fieldToSort(field, tableMeta, kind, useFunctionFormat)!;
  1069. newEventView.sorts = [sort];
  1070. return newEventView;
  1071. }
  1072. getYAxisOptions(): SelectValue<string>[] {
  1073. // Make option set and add the default options in.
  1074. return uniqBy(
  1075. this.getAggregateFields()
  1076. // Only include aggregates that make sense to be graphable (eg. not string or date)
  1077. .filter(
  1078. (field: Field) =>
  1079. isLegalYAxisType(aggregateOutputType(field.field)) ||
  1080. isAggregateEquation(field.field)
  1081. )
  1082. .map((field: Field) => ({
  1083. label: isEquation(field.field) ? getEquation(field.field) : field.field,
  1084. value: field.field,
  1085. }))
  1086. .concat(CHART_AXIS_OPTIONS),
  1087. 'value'
  1088. );
  1089. }
  1090. getYAxis(): string {
  1091. const yAxisOptions = this.getYAxisOptions();
  1092. const yAxis = this.yAxis;
  1093. const defaultOption = yAxisOptions[0].value;
  1094. if (!yAxis) {
  1095. return defaultOption;
  1096. }
  1097. // ensure current selected yAxis is one of the items in yAxisOptions
  1098. const result = yAxisOptions.findIndex(
  1099. (option: SelectValue<string>) => option.value === yAxis
  1100. );
  1101. if (result >= 0) {
  1102. return yAxis;
  1103. }
  1104. return defaultOption;
  1105. }
  1106. getDisplayOptions(): SelectValue<string>[] {
  1107. return DISPLAY_MODE_OPTIONS.map(item => {
  1108. if (item.value === DisplayModes.PREVIOUS) {
  1109. if (this.start || this.end) {
  1110. return {...item, disabled: true};
  1111. }
  1112. }
  1113. if (item.value === DisplayModes.TOP5 || item.value === DisplayModes.DAILYTOP5) {
  1114. if (this.getAggregateFields().length === 0) {
  1115. return {
  1116. ...item,
  1117. disabled: true,
  1118. tooltip: t('Add a function that groups events to use this view.'),
  1119. };
  1120. }
  1121. }
  1122. if (item.value === DisplayModes.DAILY || item.value === DisplayModes.DAILYTOP5) {
  1123. if (this.getDays() < 1) {
  1124. return {
  1125. ...item,
  1126. disabled: true,
  1127. tooltip: t('Change the date rage to at least 1 day to use this view.'),
  1128. };
  1129. }
  1130. }
  1131. return item;
  1132. });
  1133. }
  1134. getDisplayMode() {
  1135. const mode = this.display ?? DisplayModes.DEFAULT;
  1136. const displayOptions = this.getDisplayOptions();
  1137. let display = (Object.values(DisplayModes) as string[]).includes(mode)
  1138. ? mode
  1139. : DisplayModes.DEFAULT;
  1140. const cond = option => option.value === display;
  1141. // Just in case we define a fallback chain that results in an infinite loop.
  1142. // The number 5 isn't anything special, its just larger than the longest fallback
  1143. // chain that exists and isn't too big.
  1144. for (let i = 0; i < 5; i++) {
  1145. const selectedOption = displayOptions.find(cond);
  1146. if (selectedOption && !selectedOption.disabled) {
  1147. return display;
  1148. }
  1149. display = DISPLAY_MODE_FALLBACK_OPTIONS[display];
  1150. }
  1151. // after trying to find an enabled display mode and failing to find one,
  1152. // we just use the default display mode
  1153. return DisplayModes.DEFAULT;
  1154. }
  1155. getQueryWithAdditionalConditions() {
  1156. const {query} = this;
  1157. if (this.additionalConditions.isEmpty()) {
  1158. return query;
  1159. }
  1160. const conditions = new MutableSearch(query);
  1161. Object.entries(this.additionalConditions.filters).forEach(([tag, tagValues]) => {
  1162. const existingTagValues = conditions.getFilterValues(tag);
  1163. const newTagValues = tagValues.filter(
  1164. tagValue => !existingTagValues.includes(tagValue)
  1165. );
  1166. if (newTagValues.length) {
  1167. conditions.addFilterValues(tag, newTagValues);
  1168. }
  1169. });
  1170. return conditions.formatString();
  1171. }
  1172. }
  1173. export type ImmutableEventView = Readonly<Omit<EventView, 'additionalConditions'>>;
  1174. const isFieldsSimilar = (
  1175. currentValue: Array<string>,
  1176. otherValue: Array<string>
  1177. ): boolean => {
  1178. // For equation's their order matters because we alias them based on index
  1179. const currentEquations = currentValue.filter(isEquation);
  1180. const otherEquations = otherValue.filter(isEquation);
  1181. // Field orders don't matter, so using a set for comparison
  1182. const currentFields = new Set(currentValue.filter(value => !isEquation(value)));
  1183. const otherFields = new Set(otherValue.filter(value => !isEquation(value)));
  1184. if (!isEqual(currentEquations, otherEquations)) {
  1185. return false;
  1186. }
  1187. if (!isEqual(currentFields, otherFields)) {
  1188. return false;
  1189. }
  1190. return true;
  1191. };
  1192. export const isAPIPayloadSimilar = (
  1193. current: EventQuery & LocationQuery,
  1194. other: EventQuery & LocationQuery
  1195. ): boolean => {
  1196. const currentKeys = new Set(Object.keys(current));
  1197. const otherKeys = new Set(Object.keys(other));
  1198. if (!isEqual(currentKeys, otherKeys)) {
  1199. return false;
  1200. }
  1201. for (const key of currentKeys) {
  1202. const currentValue = current[key];
  1203. const otherValue = other[key];
  1204. if (key === 'field') {
  1205. if (!isFieldsSimilar(currentValue, otherValue)) {
  1206. return false;
  1207. }
  1208. } else {
  1209. const currentTarget = Array.isArray(currentValue)
  1210. ? new Set(currentValue)
  1211. : currentValue;
  1212. const otherTarget = Array.isArray(otherValue) ? new Set(otherValue) : otherValue;
  1213. if (!isEqual(currentTarget, otherTarget)) {
  1214. return false;
  1215. }
  1216. }
  1217. }
  1218. return true;
  1219. };
  1220. export function pickRelevantLocationQueryStrings(location: Location) {
  1221. const query = location.query || {};
  1222. const picked = pick(query || {}, EXTERNAL_QUERY_STRING_KEYS);
  1223. return picked;
  1224. }
  1225. export default EventView;