eventView.tsx 42 KB

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