utils.tsx 21 KB

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