utils.tsx 23 KB

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