eventView.tsx 44 KB

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