utils.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. import {browserHistory, InjectedRouter} from 'react-router';
  2. import {urlEncode} from '@sentry/utils';
  3. import {Location, Query} from 'history';
  4. import isString from 'lodash/isString';
  5. import * as Papa from 'papaparse';
  6. import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
  7. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  8. import {URL_PARAM} from 'sentry/constants/pageFilters';
  9. import {t} from 'sentry/locale';
  10. import {
  11. NewQuery,
  12. Organization,
  13. OrganizationSummary,
  14. Project,
  15. SelectValue,
  16. } from 'sentry/types';
  17. import {Event} from 'sentry/types/event';
  18. import {getUtcDateString} from 'sentry/utils/dates';
  19. import {TableDataRow} from 'sentry/utils/discover/discoverQuery';
  20. import EventView, {EventData} from 'sentry/utils/discover/eventView';
  21. import {
  22. aggregateFunctionOutputType,
  23. Aggregation,
  24. AGGREGATIONS,
  25. Column,
  26. ColumnType,
  27. ColumnValueType,
  28. explodeFieldString,
  29. Field,
  30. getAggregateAlias,
  31. getAggregateArg,
  32. getColumnsAndAggregates,
  33. getEquation,
  34. isAggregateEquation,
  35. isEquation,
  36. isMeasurement,
  37. isSpanOperationBreakdownField,
  38. measurementType,
  39. PROFILING_FIELDS,
  40. TRACING_FIELDS,
  41. } from 'sentry/utils/discover/fields';
  42. import {DisplayModes, TOP_N} from 'sentry/utils/discover/types';
  43. import {getTitle} from 'sentry/utils/events';
  44. import {DISCOVER_FIELDS, FieldValueType, getFieldDefinition} from 'sentry/utils/fields';
  45. import localStorage from 'sentry/utils/localStorage';
  46. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  47. import {DashboardWidgetSource, DisplayType, WidgetQuery} from '../dashboards/types';
  48. import {transactionSummaryRouteWithQuery} from '../performance/transactionSummary/utils';
  49. import {displayModeToDisplayType} from './savedQuery/utils';
  50. import {FieldValue, FieldValueKind, TableColumn} from './table/types';
  51. import {ALL_VIEWS, TRANSACTION_VIEWS, WEB_VITALS_VIEWS} from './data';
  52. export type QueryWithColumnState =
  53. | Query
  54. | {
  55. field: string | string[] | null | undefined;
  56. sort: string | string[] | null | undefined;
  57. };
  58. const TEMPLATE_TABLE_COLUMN: TableColumn<string> = {
  59. key: '',
  60. name: '',
  61. type: 'never',
  62. isSortable: false,
  63. column: Object.freeze({kind: 'field', field: ''}),
  64. width: COL_WIDTH_UNDEFINED,
  65. };
  66. // TODO(mark) these types are coupled to the gridEditable component types and
  67. // I'd prefer the types to be more general purpose but that will require a second pass.
  68. export function decodeColumnOrder(fields: Readonly<Field[]>): TableColumn<string>[] {
  69. return fields.map((f: Field) => {
  70. const column: TableColumn<string> = {...TEMPLATE_TABLE_COLUMN};
  71. const col = explodeFieldString(f.field, f.alias);
  72. const columnName = f.field;
  73. if (isEquation(f.field)) {
  74. column.key = f.field;
  75. column.name = getEquation(columnName);
  76. column.type = 'number';
  77. } else {
  78. column.key = columnName;
  79. column.name = columnName;
  80. }
  81. column.width = f.width || COL_WIDTH_UNDEFINED;
  82. if (col.kind === 'function') {
  83. // Aggregations can have a strict outputType or they can inherit from their field.
  84. // Otherwise use the FIELDS data to infer types.
  85. const outputType = aggregateFunctionOutputType(col.function[0], col.function[1]);
  86. if (outputType !== null) {
  87. column.type = outputType;
  88. }
  89. const aggregate = AGGREGATIONS[col.function[0]];
  90. column.isSortable = aggregate && aggregate.isSortable;
  91. } else if (col.kind === 'field') {
  92. if (getFieldDefinition(col.field) !== null) {
  93. column.type = getFieldDefinition(col.field)?.valueType as ColumnValueType;
  94. } else if (isMeasurement(col.field)) {
  95. column.type = measurementType(col.field);
  96. } else if (isSpanOperationBreakdownField(col.field)) {
  97. column.type = 'duration';
  98. }
  99. }
  100. column.column = col;
  101. return column;
  102. });
  103. }
  104. export function pushEventViewToLocation(props: {
  105. location: Location;
  106. nextEventView: EventView;
  107. extraQuery?: Query;
  108. }) {
  109. const {location, nextEventView} = props;
  110. const extraQuery = props.extraQuery || {};
  111. const queryStringObject = nextEventView.generateQueryStringObject();
  112. browserHistory.push({
  113. ...location,
  114. query: {
  115. ...extraQuery,
  116. ...queryStringObject,
  117. },
  118. });
  119. }
  120. export function generateTitle({
  121. eventView,
  122. event,
  123. organization,
  124. }: {
  125. eventView: EventView;
  126. event?: Event;
  127. organization?: Organization;
  128. }) {
  129. const titles = [t('Discover')];
  130. const eventViewName = eventView.name;
  131. if (typeof eventViewName === 'string' && String(eventViewName).trim().length > 0) {
  132. titles.push(String(eventViewName).trim());
  133. }
  134. const eventTitle = event ? getTitle(event, organization?.features).title : undefined;
  135. if (eventTitle) {
  136. titles.push(eventTitle);
  137. }
  138. titles.reverse();
  139. return titles.join(' — ');
  140. }
  141. export function getPrebuiltQueries(organization: Organization) {
  142. const views = [...ALL_VIEWS];
  143. if (organization.features.includes('performance-view')) {
  144. // insert transactions queries at index 2
  145. views.splice(2, 0, ...TRANSACTION_VIEWS);
  146. views.push(...WEB_VITALS_VIEWS);
  147. }
  148. return views;
  149. }
  150. function disableMacros(value: string | null | boolean | number) {
  151. const unsafeCharacterRegex = /^[\=\+\-\@]/;
  152. if (typeof value === 'string' && `${value}`.match(unsafeCharacterRegex)) {
  153. return `'${value}`;
  154. }
  155. return value;
  156. }
  157. export function downloadAsCsv(tableData, columnOrder, filename) {
  158. const {data} = tableData;
  159. const headings = columnOrder.map(column => column.name);
  160. const keys = columnOrder.map(column => column.key);
  161. const csvContent = Papa.unparse({
  162. fields: headings,
  163. data: data.map(row =>
  164. keys.map(key => {
  165. return disableMacros(row[key]);
  166. })
  167. ),
  168. });
  169. // Need to also manually replace # since encodeURI skips them
  170. const encodedDataUrl = `data:text/csv;charset=utf8,${encodeURIComponent(csvContent)}`;
  171. // Create a download link then click it, this is so we can get a filename
  172. const link = document.createElement('a');
  173. const now = new Date();
  174. link.setAttribute('href', encodedDataUrl);
  175. link.setAttribute('download', `${filename} ${getUtcDateString(now)}.csv`);
  176. link.click();
  177. link.remove();
  178. // Make testing easier
  179. return encodedDataUrl;
  180. }
  181. const ALIASED_AGGREGATES_COLUMN = {
  182. last_seen: 'timestamp',
  183. failure_count: 'transaction.status',
  184. };
  185. /**
  186. * Convert an aggregate into the resulting column from a drilldown action.
  187. * The result is null if the drilldown results in the aggregate being removed.
  188. */
  189. function drilldownAggregate(
  190. func: Extract<Column, {kind: 'function'}>
  191. ): Extract<Column, {kind: 'field'}> | null {
  192. const key = func.function[0];
  193. const aggregation = AGGREGATIONS[key];
  194. let column = func.function[1];
  195. if (ALIASED_AGGREGATES_COLUMN.hasOwnProperty(key)) {
  196. // Some aggregates are just shortcuts to other aggregates with
  197. // predefined arguments so we can directly map them to the result.
  198. column = ALIASED_AGGREGATES_COLUMN[key];
  199. } else if (aggregation?.parameters?.[0]) {
  200. const parameter = aggregation.parameters[0];
  201. if (parameter.kind !== 'column') {
  202. // The aggregation does not accept a column as a parameter,
  203. // so we clear the column.
  204. column = '';
  205. } else if (!column && parameter.required === false) {
  206. // The parameter was not given for a non-required parameter,
  207. // so we fall back to the default.
  208. column = parameter.defaultValue;
  209. }
  210. } else {
  211. // The aggregation does not exist or does not have any parameters,
  212. // so we clear the column.
  213. column = '';
  214. }
  215. return column ? {kind: 'field', field: column} : null;
  216. }
  217. /**
  218. * Convert an aggregated query into one that does not have aggregates.
  219. * Will also apply additions conditions defined in `additionalConditions`
  220. * and generate conditions based on the `dataRow` parameter and the current fields
  221. * in the `eventView`.
  222. */
  223. export function getExpandedResults(
  224. eventView: EventView,
  225. additionalConditions: Record<string, string>,
  226. dataRow?: TableDataRow | Event
  227. ): EventView {
  228. const fieldSet = new Set();
  229. // Expand any functions in the resulting column, and dedupe the result.
  230. // Mark any column as null to remove it.
  231. const expandedColumns: (Column | null)[] = eventView.fields.map((field: Field) => {
  232. const exploded = explodeFieldString(field.field, field.alias);
  233. const column = exploded.kind === 'function' ? drilldownAggregate(exploded) : exploded;
  234. if (
  235. // if expanding the function failed
  236. column === null ||
  237. // the new column is already present
  238. fieldSet.has(column.field) ||
  239. // Skip aggregate equations, their functions will already be added so we just want to remove it
  240. isAggregateEquation(field.field)
  241. ) {
  242. return null;
  243. }
  244. fieldSet.add(column.field);
  245. return column;
  246. });
  247. // id should be default column when expanded results in no columns; but only if
  248. // the Discover query's columns is non-empty.
  249. // This typically occurs in Discover drilldowns.
  250. if (fieldSet.size === 0 && expandedColumns.length) {
  251. expandedColumns[0] = {kind: 'field', field: 'id'};
  252. }
  253. // update the columns according the the expansion above
  254. const nextView = expandedColumns.reduceRight(
  255. (newView, column, index) =>
  256. column === null
  257. ? newView.withDeletedColumn(index, undefined)
  258. : newView.withUpdatedColumn(index, column, undefined),
  259. eventView.clone()
  260. );
  261. nextView.query = generateExpandedConditions(nextView, additionalConditions, dataRow);
  262. return nextView;
  263. }
  264. /**
  265. * Create additional conditions based on the fields in an EventView
  266. * and a datarow/event
  267. */
  268. function generateAdditionalConditions(
  269. eventView: EventView,
  270. dataRow?: TableDataRow | Event
  271. ): Record<string, string | string[]> {
  272. const specialKeys = Object.values(URL_PARAM);
  273. const conditions: Record<string, string | string[]> = {};
  274. if (!dataRow) {
  275. return conditions;
  276. }
  277. eventView.fields.forEach((field: Field) => {
  278. const column = explodeFieldString(field.field, field.alias);
  279. // Skip aggregate fields
  280. if (column.kind === 'function') {
  281. return;
  282. }
  283. const dataKey = getAggregateAlias(field.field);
  284. // Append the current field as a condition if it exists in the dataRow
  285. // Or is a simple key in the event. More complex deeply nested fields are
  286. // more challenging to get at as their location in the structure does not
  287. // match their name.
  288. if (dataRow.hasOwnProperty(dataKey)) {
  289. let value = dataRow[dataKey];
  290. if (Array.isArray(value)) {
  291. if (value.length > 1) {
  292. conditions[column.field] = value;
  293. return;
  294. }
  295. // An array with only one value is equivalent to the value itself.
  296. value = value[0];
  297. }
  298. // if the value will be quoted, then do not trim it as the whitespaces
  299. // may be important to the query and should not be trimmed
  300. const shouldQuote =
  301. value === null || value === undefined
  302. ? false
  303. : /[\s\(\)\\"]/g.test(String(value).trim());
  304. const nextValue =
  305. value === null || value === undefined
  306. ? ''
  307. : shouldQuote
  308. ? String(value)
  309. : String(value).trim();
  310. if (isMeasurement(column.field) && !nextValue) {
  311. // Do not add measurement conditions if nextValue is falsey.
  312. // It's expected that nextValue is a numeric value.
  313. return;
  314. }
  315. switch (column.field) {
  316. case 'timestamp':
  317. // normalize the "timestamp" field to ensure the payload works
  318. conditions[column.field] = getUtcDateString(nextValue);
  319. break;
  320. default:
  321. conditions[column.field] = nextValue;
  322. }
  323. }
  324. // If we have an event, check tags as well.
  325. if (dataRow.tags && Array.isArray(dataRow.tags)) {
  326. const tagIndex = dataRow.tags.findIndex(item => item.key === dataKey);
  327. if (tagIndex > -1) {
  328. const key = specialKeys.includes(column.field)
  329. ? `tags[${column.field}]`
  330. : column.field;
  331. const tagValue = dataRow.tags[tagIndex].value;
  332. conditions[key] = tagValue;
  333. }
  334. }
  335. });
  336. return conditions;
  337. }
  338. /**
  339. * Discover queries can query either Errors, Transactions or a combination
  340. * of the two datasets. This is a util to determine if the query will excusively
  341. * hit the Transactions dataset.
  342. */
  343. export function usesTransactionsDataset(eventView: EventView, yAxisValue: string[]) {
  344. let usesTransactions: boolean = false;
  345. const parsedQuery = new MutableSearch(eventView.query);
  346. for (let index = 0; index < yAxisValue.length; index++) {
  347. const yAxis = yAxisValue[index];
  348. const aggregateArg = getAggregateArg(yAxis) ?? '';
  349. if (isMeasurement(aggregateArg) || aggregateArg === 'transaction.duration') {
  350. usesTransactions = true;
  351. break;
  352. }
  353. const eventTypeFilter = parsedQuery.getFilterValues('event.type');
  354. if (
  355. eventTypeFilter.length > 0 &&
  356. eventTypeFilter.every(filter => filter === 'transaction')
  357. ) {
  358. usesTransactions = true;
  359. break;
  360. }
  361. }
  362. return usesTransactions;
  363. }
  364. function generateExpandedConditions(
  365. eventView: EventView,
  366. additionalConditions: Record<string, string>,
  367. dataRow?: TableDataRow | Event
  368. ): string {
  369. const parsedQuery = new MutableSearch(eventView.query);
  370. // Remove any aggregates from the search conditions.
  371. // otherwise, it'll lead to an invalid query result.
  372. for (const key in parsedQuery.filters) {
  373. const column = explodeFieldString(key);
  374. if (column.kind === 'function') {
  375. parsedQuery.removeFilter(key);
  376. }
  377. }
  378. const conditions: Record<string, string | string[]> = Object.assign(
  379. {},
  380. additionalConditions,
  381. generateAdditionalConditions(eventView, dataRow)
  382. );
  383. // Add additional conditions provided and generated.
  384. for (const key in conditions) {
  385. const value = conditions[key];
  386. if (Array.isArray(value)) {
  387. parsedQuery.setFilterValues(key, value);
  388. continue;
  389. }
  390. if (key === 'project.id') {
  391. eventView.project = [...eventView.project, parseInt(value, 10)];
  392. continue;
  393. }
  394. if (key === 'environment') {
  395. if (!eventView.environment.includes(value)) {
  396. eventView.environment = [...eventView.environment, value];
  397. }
  398. continue;
  399. }
  400. const column = explodeFieldString(key);
  401. // Skip aggregates as they will be invalid.
  402. if (column.kind === 'function') {
  403. continue;
  404. }
  405. parsedQuery.setFilterValues(key, [value]);
  406. }
  407. return parsedQuery.formatString();
  408. }
  409. type FieldGeneratorOpts = {
  410. organization: OrganizationSummary;
  411. aggregations?: Record<string, Aggregation>;
  412. customMeasurements?: {functions: string[]; key: string}[] | null;
  413. fieldKeys?: string[];
  414. measurementKeys?: string[] | null;
  415. spanOperationBreakdownKeys?: string[];
  416. tagKeys?: string[] | null;
  417. };
  418. export function generateFieldOptions({
  419. organization,
  420. tagKeys,
  421. measurementKeys,
  422. spanOperationBreakdownKeys,
  423. customMeasurements,
  424. aggregations = AGGREGATIONS,
  425. fieldKeys = DISCOVER_FIELDS,
  426. }: FieldGeneratorOpts) {
  427. let functions = Object.keys(aggregations);
  428. // Strip tracing features if the org doesn't have access.
  429. if (!organization.features.includes('performance-view')) {
  430. fieldKeys = fieldKeys.filter(item => !TRACING_FIELDS.includes(item));
  431. functions = functions.filter(item => !TRACING_FIELDS.includes(item));
  432. }
  433. // Strip profiling features if the org doesn't have access.
  434. if (!organization.features.includes('profiling')) {
  435. fieldKeys = fieldKeys.filter(item => !PROFILING_FIELDS.includes(item));
  436. }
  437. // Strip device.class if the org doesn't have access.
  438. if (!organization.features.includes('device-classification')) {
  439. fieldKeys = fieldKeys.filter(item => item !== 'device.class');
  440. }
  441. const fieldOptions: Record<string, SelectValue<FieldValue>> = {};
  442. // Index items by prefixed keys as custom tags can overlap both fields and
  443. // function names. Having a mapping makes finding the value objects easier
  444. // later as well.
  445. functions.forEach(func => {
  446. const ellipsis = aggregations[func].parameters.length ? '\u2026' : '';
  447. const parameters = aggregations[func].parameters.map(param => {
  448. const overrides = AGGREGATIONS[func].getFieldOverrides;
  449. if (typeof overrides === 'undefined') {
  450. return param;
  451. }
  452. return {
  453. ...param,
  454. ...overrides({parameter: param}),
  455. };
  456. });
  457. fieldOptions[`function:${func}`] = {
  458. label: `${func}(${ellipsis})`,
  459. value: {
  460. kind: FieldValueKind.FUNCTION,
  461. meta: {
  462. name: func,
  463. parameters,
  464. },
  465. },
  466. };
  467. });
  468. fieldKeys.forEach(field => {
  469. fieldOptions[`field:${field}`] = {
  470. label: field,
  471. value: {
  472. kind: FieldValueKind.FIELD,
  473. meta: {
  474. name: field,
  475. dataType: (getFieldDefinition(field)?.valueType ??
  476. FieldValueType.STRING) as ColumnType,
  477. },
  478. },
  479. };
  480. });
  481. if (measurementKeys !== undefined && measurementKeys !== null) {
  482. measurementKeys.sort();
  483. measurementKeys.forEach(measurement => {
  484. fieldOptions[`measurement:${measurement}`] = {
  485. label: measurement,
  486. value: {
  487. kind: FieldValueKind.MEASUREMENT,
  488. meta: {name: measurement, dataType: measurementType(measurement)},
  489. },
  490. };
  491. });
  492. }
  493. if (customMeasurements !== undefined && customMeasurements !== null) {
  494. customMeasurements.sort(({key: currentKey}, {key: nextKey}) =>
  495. currentKey > nextKey ? 1 : currentKey === nextKey ? 0 : -1
  496. );
  497. customMeasurements.forEach(({key, functions: supportedFunctions}) => {
  498. fieldOptions[`measurement:${key}`] = {
  499. label: key,
  500. value: {
  501. kind: FieldValueKind.CUSTOM_MEASUREMENT,
  502. meta: {
  503. name: key,
  504. dataType: measurementType(key),
  505. functions: supportedFunctions,
  506. },
  507. },
  508. };
  509. });
  510. }
  511. if (Array.isArray(spanOperationBreakdownKeys)) {
  512. spanOperationBreakdownKeys.sort();
  513. spanOperationBreakdownKeys.forEach(breakdownField => {
  514. fieldOptions[`span_op_breakdown:${breakdownField}`] = {
  515. label: breakdownField,
  516. value: {
  517. kind: FieldValueKind.BREAKDOWN,
  518. meta: {name: breakdownField, dataType: 'duration'},
  519. },
  520. };
  521. });
  522. }
  523. if (tagKeys !== undefined && tagKeys !== null) {
  524. tagKeys.sort();
  525. tagKeys.forEach(tag => {
  526. const tagValue =
  527. fieldKeys.includes(tag) || AGGREGATIONS.hasOwnProperty(tag)
  528. ? `tags[${tag}]`
  529. : tag;
  530. fieldOptions[`tag:${tag}`] = {
  531. label: tag,
  532. value: {
  533. kind: FieldValueKind.TAG,
  534. meta: {name: tagValue, dataType: 'string'},
  535. },
  536. };
  537. });
  538. }
  539. return fieldOptions;
  540. }
  541. const RENDER_PREBUILT_KEY = 'discover-render-prebuilt';
  542. export function shouldRenderPrebuilt(): boolean {
  543. const shouldRender = localStorage.getItem(RENDER_PREBUILT_KEY);
  544. return shouldRender === 'true' || shouldRender === null;
  545. }
  546. export function setRenderPrebuilt(value: boolean) {
  547. localStorage.setItem(RENDER_PREBUILT_KEY, value ? 'true' : 'false');
  548. }
  549. export function eventViewToWidgetQuery({
  550. eventView,
  551. yAxis,
  552. displayType,
  553. }: {
  554. displayType: DisplayType;
  555. eventView: EventView;
  556. yAxis?: string | string[];
  557. }) {
  558. const fields = eventView.fields.map(({field}) => field);
  559. const {columns, aggregates} = getColumnsAndAggregates(fields);
  560. const sort = eventView.sorts[0];
  561. const queryYAxis = typeof yAxis === 'string' ? [yAxis] : yAxis ?? ['count()'];
  562. let orderby = '';
  563. // The orderby should only be set to sort.field if it is a Top N query
  564. // since the query uses all of the fields, or if the ordering is used in the y-axis
  565. if (sort) {
  566. let orderbyFunction = '';
  567. const aggregateFields = [...queryYAxis, ...aggregates];
  568. for (let i = 0; i < aggregateFields.length; i++) {
  569. if (sort.field === getAggregateAlias(aggregateFields[i])) {
  570. orderbyFunction = aggregateFields[i];
  571. break;
  572. }
  573. }
  574. const bareOrderby = orderbyFunction === '' ? sort.field : orderbyFunction;
  575. if (displayType === DisplayType.TOP_N || bareOrderby) {
  576. orderby = `${sort.kind === 'desc' ? '-' : ''}${bareOrderby}`;
  577. }
  578. }
  579. let newAggregates = aggregates;
  580. if (displayType !== DisplayType.TABLE) {
  581. newAggregates = queryYAxis;
  582. }
  583. const widgetQuery: WidgetQuery = {
  584. name: '',
  585. aggregates: newAggregates,
  586. columns: [...(displayType === DisplayType.TOP_N ? columns : [])],
  587. fields: [...(displayType === DisplayType.TOP_N ? fields : []), ...queryYAxis],
  588. conditions: eventView.query,
  589. orderby,
  590. };
  591. return widgetQuery;
  592. }
  593. export function handleAddQueryToDashboard({
  594. eventView,
  595. location,
  596. query,
  597. organization,
  598. router,
  599. yAxis,
  600. }: {
  601. eventView: EventView;
  602. location: Location;
  603. organization: Organization;
  604. router: InjectedRouter;
  605. query?: NewQuery;
  606. yAxis?: string | string[];
  607. }) {
  608. const displayType = displayModeToDisplayType(eventView.display as DisplayModes);
  609. const defaultWidgetQuery = eventViewToWidgetQuery({
  610. eventView,
  611. displayType,
  612. yAxis,
  613. });
  614. const {query: widgetAsQueryParams} = constructAddQueryToDashboardLink({
  615. eventView,
  616. query,
  617. organization,
  618. yAxis,
  619. location,
  620. });
  621. openAddToDashboardModal({
  622. organization,
  623. selection: {
  624. projects: eventView.project,
  625. environments: eventView.environment,
  626. datetime: {
  627. start: eventView.start,
  628. end: eventView.end,
  629. period: eventView.statsPeriod,
  630. utc: eventView.utc,
  631. },
  632. },
  633. widget: {
  634. title: query?.name ?? eventView.name,
  635. displayType: displayType === DisplayType.TOP_N ? DisplayType.AREA : displayType,
  636. queries: [
  637. {
  638. ...defaultWidgetQuery,
  639. aggregates: [...(typeof yAxis === 'string' ? [yAxis] : yAxis ?? ['count()'])],
  640. },
  641. ],
  642. interval: eventView.interval,
  643. limit:
  644. displayType === DisplayType.TOP_N
  645. ? Number(eventView.topEvents) || TOP_N
  646. : undefined,
  647. },
  648. router,
  649. widgetAsQueryParams,
  650. location,
  651. });
  652. return;
  653. }
  654. export function getTargetForTransactionSummaryLink(
  655. dataRow: EventData,
  656. organization: Organization,
  657. projects?: Project[],
  658. nextView?: EventView,
  659. location?: Location
  660. ) {
  661. let projectID: string | string[] | undefined;
  662. const filterProjects = location?.query.project;
  663. if (isString(filterProjects) && filterProjects !== '-1') {
  664. // Project selector in discover has just one selected project
  665. projectID = filterProjects;
  666. } else {
  667. const projectMatch = projects?.find(
  668. project =>
  669. project.slug && [dataRow['project.name'], dataRow.project].includes(project.slug)
  670. );
  671. projectID = projectMatch ? [projectMatch.id] : undefined;
  672. }
  673. const target = transactionSummaryRouteWithQuery({
  674. orgSlug: organization.slug,
  675. transaction: String(dataRow.transaction),
  676. projectID,
  677. query: nextView?.getPageFiltersQuery() || {},
  678. });
  679. // Pass on discover filter params when there are multiple
  680. // projects associated with the transaction
  681. if (!projectID && filterProjects) {
  682. target.query.project = filterProjects;
  683. }
  684. return target;
  685. }
  686. export function constructAddQueryToDashboardLink({
  687. eventView,
  688. query,
  689. organization,
  690. yAxis,
  691. location,
  692. }: {
  693. eventView: EventView;
  694. organization: Organization;
  695. location?: Location;
  696. query?: NewQuery;
  697. yAxis?: string | string[];
  698. }) {
  699. const displayType = displayModeToDisplayType(eventView.display as DisplayModes);
  700. const defaultTableFields = eventView.fields.map(({field}) => field);
  701. const defaultWidgetQuery = eventViewToWidgetQuery({
  702. eventView,
  703. displayType,
  704. yAxis,
  705. });
  706. const defaultTitle =
  707. query?.name ?? (eventView.name !== 'All Events' ? eventView.name : undefined);
  708. return {
  709. pathname: `/organizations/${organization.slug}/dashboards/new/widget/new/`,
  710. query: {
  711. ...location?.query,
  712. source: DashboardWidgetSource.DISCOVERV2,
  713. start: eventView.start,
  714. end: eventView.end,
  715. statsPeriod: eventView.statsPeriod,
  716. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  717. defaultTableColumns: defaultTableFields,
  718. defaultTitle,
  719. displayType: displayType === DisplayType.TOP_N ? DisplayType.AREA : displayType,
  720. limit:
  721. displayType === DisplayType.TOP_N
  722. ? Number(eventView.topEvents) || TOP_N
  723. : undefined,
  724. },
  725. };
  726. }