utils.tsx 23 KB

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