results.tsx 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import type {Location} from 'history';
  5. import isEqual from 'lodash/isEqual';
  6. import omit from 'lodash/omit';
  7. import {updateSavedQueryVisit} from 'sentry/actionCreators/discoverSavedQueries';
  8. import {fetchTotalCount} from 'sentry/actionCreators/events';
  9. import {fetchProjectsCount} from 'sentry/actionCreators/projects';
  10. import {loadOrganizationTags} from 'sentry/actionCreators/tags';
  11. import {Client} from 'sentry/api';
  12. import {Alert} from 'sentry/components/alert';
  13. import {Button} from 'sentry/components/button';
  14. import Confirm from 'sentry/components/confirm';
  15. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  16. import * as Layout from 'sentry/components/layouts/thirds';
  17. import ExternalLink from 'sentry/components/links/externalLink';
  18. import LoadingIndicator from 'sentry/components/loadingIndicator';
  19. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  20. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  21. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  22. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  23. import {
  24. normalizeDateTimeParams,
  25. normalizeDateTimeString,
  26. } from 'sentry/components/organizations/pageFilters/parse';
  27. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  28. import type {CursorHandler} from 'sentry/components/pagination';
  29. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  30. import {IconClose} from 'sentry/icons/iconClose';
  31. import {t, tct} from 'sentry/locale';
  32. import {space} from 'sentry/styles/space';
  33. import type {PageFilters} from 'sentry/types/core';
  34. import {SavedSearchType} from 'sentry/types/group';
  35. import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
  36. import type {NewQuery, Organization, SavedQuery} from 'sentry/types/organization';
  37. import {defined, generateQueryWithTag} from 'sentry/utils';
  38. import {trackAnalytics} from 'sentry/utils/analytics';
  39. import {browserHistory} from 'sentry/utils/browserHistory';
  40. import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
  41. import {CustomMeasurementsContext} from 'sentry/utils/customMeasurements/customMeasurementsContext';
  42. import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider';
  43. import EventView, {isAPIPayloadSimilar} from 'sentry/utils/discover/eventView';
  44. import {formatTagKey, generateAggregateFields} from 'sentry/utils/discover/fields';
  45. import {
  46. DatasetSource,
  47. DiscoverDatasets,
  48. DisplayModes,
  49. MULTI_Y_AXIS_SUPPORTED_DISPLAY_MODES,
  50. SavedQueryDatasets,
  51. } from 'sentry/utils/discover/types';
  52. import localStorage from 'sentry/utils/localStorage';
  53. import marked from 'sentry/utils/marked';
  54. import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
  55. import {decodeList, decodeScalar} from 'sentry/utils/queryString';
  56. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  57. import withApi from 'sentry/utils/withApi';
  58. import withOrganization from 'sentry/utils/withOrganization';
  59. import withPageFilters from 'sentry/utils/withPageFilters';
  60. import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
  61. import {DATASET_LABEL_MAP} from 'sentry/views/discover/savedQuery/datasetSelectorTabs';
  62. import {
  63. getDatasetFromLocationOrSavedQueryDataset,
  64. getSavedQueryDataset,
  65. getSavedQueryWithDataset,
  66. } from 'sentry/views/discover/savedQuery/utils';
  67. import {addRoutePerformanceContext} from '../performance/utils';
  68. import {DEFAULT_EVENT_VIEW, DEFAULT_EVENT_VIEW_MAP} from './data';
  69. import ResultsChart from './resultsChart';
  70. import ResultsHeader from './resultsHeader';
  71. import ResultsSearchQueryBuilder from './resultsSearchQueryBuilder';
  72. import {SampleDataAlert} from './sampleDataAlert';
  73. import Table from './table';
  74. import Tags from './tags';
  75. import {generateTitle} from './utils';
  76. type Props = {
  77. api: Client;
  78. loading: boolean;
  79. location: Location;
  80. organization: Organization;
  81. router: InjectedRouter;
  82. selection: PageFilters;
  83. setSavedQuery: (savedQuery?: SavedQuery) => void;
  84. isHomepage?: boolean;
  85. savedQuery?: SavedQuery;
  86. };
  87. type State = {
  88. confirmedQuery: boolean;
  89. error: string;
  90. errorCode: number;
  91. eventView: EventView;
  92. needConfirmation: boolean;
  93. showTags: boolean;
  94. tips: string[];
  95. totalValues: null | number;
  96. homepageQuery?: SavedQuery;
  97. savedQuery?: SavedQuery;
  98. savedQueryDataset?: SavedQueryDatasets;
  99. showForcedDatasetAlert?: boolean;
  100. showMetricsAlert?: boolean;
  101. showQueryIncompatibleWithDataset?: boolean;
  102. showUnparameterizedBanner?: boolean;
  103. splitDecision?: SavedQueryDatasets;
  104. };
  105. const SHOW_TAGS_STORAGE_KEY = 'discover2:show-tags';
  106. const SHOW_UNPARAM_BANNER = 'showUnparameterizedBanner';
  107. function readShowTagsState() {
  108. const value = localStorage.getItem(SHOW_TAGS_STORAGE_KEY);
  109. return value === '1';
  110. }
  111. function getYAxis(location: Location, eventView: EventView, savedQuery?: SavedQuery) {
  112. if (location.query.yAxis) {
  113. return decodeList(location.query.yAxis);
  114. }
  115. if (location.query.yAxis === null) {
  116. return [];
  117. }
  118. return savedQuery?.yAxis && savedQuery?.yAxis.length > 0
  119. ? decodeList(savedQuery?.yAxis)
  120. : [eventView.getYAxis()];
  121. }
  122. export class Results extends Component<Props, State> {
  123. static getDerivedStateFromProps(nextProps: Readonly<Props>, prevState: State): State {
  124. const savedQueryDataset = getSavedQueryDataset(
  125. nextProps.organization,
  126. nextProps.location,
  127. nextProps.savedQuery,
  128. undefined
  129. );
  130. const eventViewFromQuery = EventView.fromSavedQueryOrLocation(
  131. nextProps.savedQuery,
  132. nextProps.location
  133. );
  134. const eventView =
  135. hasDatasetSelector(nextProps.organization) && !eventViewFromQuery.dataset
  136. ? eventViewFromQuery.withDataset(
  137. getDatasetFromLocationOrSavedQueryDataset(undefined, savedQueryDataset)
  138. )
  139. : eventViewFromQuery;
  140. return {...prevState, eventView, savedQuery: nextProps.savedQuery, savedQueryDataset};
  141. }
  142. state: State = {
  143. eventView: EventView.fromSavedQueryOrLocation(
  144. this.props.savedQuery,
  145. this.props.location
  146. ),
  147. savedQueryDataset: getSavedQueryDataset(
  148. this.props.organization,
  149. this.props.location,
  150. this.props.savedQuery,
  151. undefined
  152. ),
  153. error: '',
  154. homepageQuery: undefined,
  155. errorCode: 200,
  156. totalValues: null,
  157. showTags: readShowTagsState(),
  158. needConfirmation: false,
  159. confirmedQuery: false,
  160. tips: [],
  161. showForcedDatasetAlert: true,
  162. showQueryIncompatibleWithDataset: false,
  163. };
  164. componentDidMount() {
  165. const {organization, selection, location, isHomepage} = this.props;
  166. if (location.query.fromMetric) {
  167. this.setState({showMetricsAlert: true});
  168. browserHistory.replace({
  169. ...location,
  170. query: {...location.query, fromMetric: undefined},
  171. });
  172. }
  173. if (location.query[SHOW_UNPARAM_BANNER]) {
  174. this.setState({showUnparameterizedBanner: true});
  175. browserHistory.replace({
  176. ...location,
  177. query: {...location.query, [SHOW_UNPARAM_BANNER]: undefined},
  178. });
  179. }
  180. loadOrganizationTags(this.tagsApi, organization.slug, selection);
  181. addRoutePerformanceContext(selection);
  182. this.checkEventView();
  183. this.canLoadEvents();
  184. if (!isHomepage && defined(location.query.id)) {
  185. updateSavedQueryVisit(organization.slug, location.query.id);
  186. }
  187. }
  188. componentDidUpdate(prevProps: Props, prevState: State) {
  189. const {location, organization, selection} = this.props;
  190. const {eventView, confirmedQuery, savedQuery} = this.state;
  191. if (location.query.incompatible) {
  192. this.setState({showQueryIncompatibleWithDataset: true});
  193. browserHistory.replace({
  194. ...location,
  195. query: {...location.query, incompatible: undefined},
  196. });
  197. }
  198. this.checkEventView();
  199. const currentQuery = eventView.getEventsAPIPayload(location);
  200. const prevQuery = prevState.eventView.getEventsAPIPayload(prevProps.location);
  201. const yAxisArray = getYAxis(location, eventView, savedQuery);
  202. const prevYAxisArray = getYAxis(prevProps.location, eventView, prevState.savedQuery);
  203. const savedQueryDataset =
  204. decodeScalar(location.query.queryDataset) ?? savedQuery?.queryDataset;
  205. const prevSavedQueryDataset =
  206. decodeScalar(prevProps.location.query.queryDataset) ??
  207. prevState.savedQuery?.queryDataset;
  208. const datasetChanged = !isEqual(savedQueryDataset, prevSavedQueryDataset);
  209. if (
  210. !isAPIPayloadSimilar(currentQuery, prevQuery) ||
  211. this.hasChartParametersChanged(
  212. prevState.eventView,
  213. eventView,
  214. prevYAxisArray,
  215. yAxisArray
  216. ) ||
  217. datasetChanged
  218. ) {
  219. this.canLoadEvents();
  220. }
  221. if (
  222. !isEqual(prevProps.selection.datetime, selection.datetime) ||
  223. !isEqual(prevProps.selection.projects, selection.projects)
  224. ) {
  225. loadOrganizationTags(this.tagsApi, organization.slug, selection);
  226. addRoutePerformanceContext(selection);
  227. }
  228. if (prevState.confirmedQuery !== confirmedQuery) {
  229. this.fetchTotalCount();
  230. }
  231. }
  232. tagsApi: Client = new Client();
  233. hasChartParametersChanged(
  234. prevEventView: EventView,
  235. eventView: EventView,
  236. prevYAxisArray: string[],
  237. yAxisArray: string[]
  238. ) {
  239. if (!isEqual(prevYAxisArray, yAxisArray)) {
  240. return true;
  241. }
  242. const prevDisplay = prevEventView.getDisplayMode();
  243. const display = eventView.getDisplayMode();
  244. return prevDisplay !== display;
  245. }
  246. canLoadEvents = async () => {
  247. const {api, location, organization} = this.props;
  248. const {eventView} = this.state;
  249. let needConfirmation = false;
  250. let confirmedQuery = true;
  251. const currentQuery = eventView.getEventsAPIPayload(location);
  252. const duration = eventView.getDays();
  253. if (duration > 30 && currentQuery.project) {
  254. let projectLength = currentQuery.project.length;
  255. if (
  256. projectLength === 0 ||
  257. (projectLength === 1 && currentQuery.project[0] === '-1')
  258. ) {
  259. try {
  260. const results = await fetchProjectsCount(api, organization.slug);
  261. if (projectLength === 0) {
  262. projectLength = results.myProjects;
  263. } else {
  264. projectLength = results.allProjects;
  265. }
  266. } catch (err) {
  267. // do nothing, so the length is 0 or 1 and the query is assumed safe
  268. }
  269. }
  270. if (projectLength > 10) {
  271. needConfirmation = true;
  272. confirmedQuery = false;
  273. }
  274. }
  275. // Once confirmed, a change of project or datetime will happen before this can set it to false,
  276. // this means a query will still happen even if the new conditions need confirmation
  277. // using a state callback to return this to false
  278. this.setState({needConfirmation, confirmedQuery}, () => {
  279. this.setState({confirmedQuery: false});
  280. });
  281. if (needConfirmation) {
  282. this.openConfirm();
  283. }
  284. };
  285. openConfirm = () => {};
  286. setOpenFunction = ({open}) => {
  287. this.openConfirm = open;
  288. return null;
  289. };
  290. handleConfirmed = () => {
  291. this.setState({needConfirmation: false, confirmedQuery: true}, () => {
  292. this.setState({confirmedQuery: false});
  293. });
  294. };
  295. handleCancelled = () => {
  296. this.setState({needConfirmation: false, confirmedQuery: false});
  297. };
  298. async fetchTotalCount() {
  299. const {api, organization, location} = this.props;
  300. const {eventView, confirmedQuery} = this.state;
  301. if (confirmedQuery === false || !eventView.isValid()) {
  302. return;
  303. }
  304. try {
  305. const totals = await fetchTotalCount(
  306. api,
  307. organization.slug,
  308. eventView.getEventsAPIPayload(location)
  309. );
  310. this.setState({totalValues: totals});
  311. } catch (err) {
  312. Sentry.captureException(err);
  313. }
  314. }
  315. checkEventView() {
  316. const {eventView, splitDecision, savedQueryDataset} = this.state;
  317. const {loading} = this.props;
  318. if (eventView.isValid() || loading) {
  319. return;
  320. }
  321. // If the view is not valid, redirect to a known valid state.
  322. const {location, organization, selection, isHomepage, savedQuery} = this.props;
  323. const value = getSavedQueryDataset(organization, location, savedQuery, splitDecision);
  324. const defaultEventView = hasDatasetSelector(organization)
  325. ? (getSavedQueryWithDataset(DEFAULT_EVENT_VIEW_MAP[value]) as NewQuery)
  326. : DEFAULT_EVENT_VIEW;
  327. const query = isHomepage && savedQuery ? omit(savedQuery, 'id') : defaultEventView;
  328. const nextEventView = EventView.fromNewQueryWithLocation(query, location);
  329. if (nextEventView.project.length === 0 && selection.projects) {
  330. nextEventView.project = selection.projects;
  331. }
  332. if (nextEventView.environment.length === 0 && selection.environments) {
  333. nextEventView.environment = selection.environments;
  334. }
  335. if (selection.datetime) {
  336. const {period, utc, start, end} = selection.datetime;
  337. nextEventView.statsPeriod = period ?? undefined;
  338. nextEventView.utc = utc?.toString();
  339. nextEventView.start = normalizeDateTimeString(start);
  340. nextEventView.end = normalizeDateTimeString(end);
  341. }
  342. if (location.query?.query) {
  343. nextEventView.query = decodeScalar(location.query.query, '');
  344. }
  345. if (isHomepage && !this.state.savedQuery) {
  346. this.setState({savedQuery, eventView: nextEventView});
  347. }
  348. browserHistory.replace(
  349. normalizeUrl(
  350. nextEventView.getResultsViewUrlTarget(
  351. organization.slug,
  352. isHomepage,
  353. hasDatasetSelector(organization) ? savedQueryDataset : undefined
  354. )
  355. )
  356. );
  357. }
  358. handleCursor: CursorHandler = (cursor, path, query, _direction) => {
  359. const {router} = this.props;
  360. router.push({
  361. pathname: path,
  362. query: {...query, cursor},
  363. });
  364. // Treat pagination like the user already confirmed the query
  365. if (!this.state.needConfirmation) {
  366. this.handleConfirmed();
  367. }
  368. };
  369. handleChangeShowTags = () => {
  370. const {organization} = this.props;
  371. trackAnalytics('discover_v2.results.toggle_tag_facets', {
  372. organization,
  373. });
  374. this.setState(state => {
  375. const newValue = !state.showTags;
  376. localStorage.setItem(SHOW_TAGS_STORAGE_KEY, newValue ? '1' : '0');
  377. return {...state, showTags: newValue};
  378. });
  379. };
  380. handleSearch = (query: string) => {
  381. const {router, location} = this.props;
  382. const queryParams = normalizeDateTimeParams({
  383. ...(location.query || {}),
  384. query,
  385. });
  386. // do not propagate pagination when making a new search
  387. const searchQueryParams = omit(queryParams, 'cursor');
  388. router.push({
  389. pathname: location.pathname,
  390. query: searchQueryParams,
  391. });
  392. };
  393. handleYAxisChange = (value: string[]) => {
  394. const {router, location} = this.props;
  395. const isDisplayMultiYAxisSupported = MULTI_Y_AXIS_SUPPORTED_DISPLAY_MODES.includes(
  396. location.query.display as DisplayModes
  397. );
  398. const newQuery = {
  399. ...location.query,
  400. yAxis: value.length > 0 ? value : [null],
  401. // If using Multi Y-axis and not in a supported display, change to the default display mode
  402. display:
  403. value.length > 1 && !isDisplayMultiYAxisSupported
  404. ? location.query.display === DisplayModes.DAILYTOP5
  405. ? DisplayModes.DAILY
  406. : DisplayModes.DEFAULT
  407. : location.query.display,
  408. };
  409. router.push({
  410. pathname: location.pathname,
  411. query: newQuery,
  412. });
  413. // Treat axis changing like the user already confirmed the query
  414. if (!this.state.needConfirmation) {
  415. this.handleConfirmed();
  416. }
  417. trackAnalytics('discover_v2.y_axis_change', {
  418. organization: this.props.organization,
  419. y_axis_value: value,
  420. });
  421. };
  422. handleDisplayChange = (value: string) => {
  423. const {router, location} = this.props;
  424. const newQuery = {
  425. ...location.query,
  426. display: value,
  427. };
  428. router.push({
  429. pathname: location.pathname,
  430. query: newQuery,
  431. });
  432. // Treat display changing like the user already confirmed the query
  433. if (!this.state.needConfirmation) {
  434. this.handleConfirmed();
  435. }
  436. };
  437. handleIntervalChange = (value: string | undefined) => {
  438. const {router, location} = this.props;
  439. const newQuery = {
  440. ...location.query,
  441. interval: value,
  442. };
  443. if (location.query.interval !== value) {
  444. router.push({
  445. pathname: location.pathname,
  446. query: newQuery,
  447. });
  448. // Treat display changing like the user already confirmed the query
  449. if (!this.state.needConfirmation) {
  450. this.handleConfirmed();
  451. }
  452. }
  453. };
  454. handleTopEventsChange = (value: string) => {
  455. const {router, location} = this.props;
  456. const newQuery = {
  457. ...location.query,
  458. topEvents: value,
  459. };
  460. router.push({
  461. pathname: location.pathname,
  462. query: newQuery,
  463. });
  464. // Treat display changing like the user already confirmed the query
  465. if (!this.state.needConfirmation) {
  466. this.handleConfirmed();
  467. }
  468. };
  469. getDocumentTitle(): string {
  470. const {eventView} = this.state;
  471. const {isHomepage} = this.props;
  472. if (!eventView) {
  473. return '';
  474. }
  475. return generateTitle({eventView, isHomepage});
  476. }
  477. renderTagsTable() {
  478. const {organization, location} = this.props;
  479. const {eventView, totalValues, confirmedQuery} = this.state;
  480. return (
  481. <Layout.Side>
  482. <Tags
  483. generateUrl={this.generateTagUrl}
  484. totalValues={totalValues}
  485. eventView={eventView}
  486. organization={organization}
  487. location={location}
  488. confirmedQuery={confirmedQuery}
  489. />
  490. </Layout.Side>
  491. );
  492. }
  493. generateTagUrl = (key: string, value: string) => {
  494. const {organization, isHomepage} = this.props;
  495. const {eventView, savedQueryDataset} = this.state;
  496. const url = eventView.getResultsViewUrlTarget(
  497. organization.slug,
  498. isHomepage,
  499. hasDatasetSelector(organization) ? savedQueryDataset : undefined
  500. );
  501. url.query = generateQueryWithTag(url.query, {
  502. key: formatTagKey(key),
  503. value,
  504. });
  505. return url;
  506. };
  507. renderError(error: string) {
  508. if (!error) {
  509. return null;
  510. }
  511. return (
  512. <Alert type="error" showIcon>
  513. {error}
  514. </Alert>
  515. );
  516. }
  517. setError = (error: string, errorCode: number) => {
  518. this.setState({error, errorCode});
  519. };
  520. renderMetricsFallbackBanner() {
  521. const {organization} = this.props;
  522. if (
  523. !organization.features.includes('performance-mep-bannerless-ui') &&
  524. this.state.showMetricsAlert
  525. ) {
  526. return (
  527. <Alert type="info" showIcon>
  528. {t(
  529. "You've navigated to this page from a performance metric widget generated from processed events. The results here only show indexed events."
  530. )}
  531. </Alert>
  532. );
  533. }
  534. if (this.state.showUnparameterizedBanner) {
  535. return (
  536. <Alert type="info" showIcon>
  537. {tct(
  538. 'These are unparameterized transactions. To better organize your transactions, [link:set transaction names manually].',
  539. {
  540. link: (
  541. <ExternalLink href="https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#beforenavigate" />
  542. ),
  543. }
  544. )}
  545. </Alert>
  546. );
  547. }
  548. return null;
  549. }
  550. renderQueryIncompatibleWithDatasetBanner() {
  551. const {organization} = this.props;
  552. if (hasDatasetSelector(organization) && this.state.showQueryIncompatibleWithDataset) {
  553. return (
  554. <Alert
  555. type="warning"
  556. showIcon
  557. trailingItems={
  558. <StyledCloseButton
  559. icon={<IconClose size="sm" />}
  560. aria-label={t('Close')}
  561. onClick={() => {
  562. this.setState({showQueryIncompatibleWithDataset: false});
  563. }}
  564. size="zero"
  565. borderless
  566. />
  567. }
  568. >
  569. {t('Your query was updated to make it compatible with this dataset.')}
  570. </Alert>
  571. );
  572. }
  573. return null;
  574. }
  575. renderForcedDatasetBanner() {
  576. const {organization, savedQuery} = this.props;
  577. if (
  578. hasDatasetSelector(organization) &&
  579. this.state.showForcedDatasetAlert &&
  580. (this.state.splitDecision || savedQuery?.datasetSource === DatasetSource.FORCED)
  581. ) {
  582. const splitDecision = this.state.splitDecision ?? savedQuery?.queryDataset;
  583. if (!splitDecision) {
  584. return null;
  585. }
  586. return (
  587. <Alert
  588. type="warning"
  589. showIcon
  590. trailingItems={
  591. <StyledCloseButton
  592. icon={<IconClose size="sm" />}
  593. aria-label={t('Close')}
  594. onClick={() => {
  595. this.setState({showForcedDatasetAlert: false});
  596. }}
  597. size="zero"
  598. borderless
  599. />
  600. }
  601. >
  602. {tct(
  603. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this query to [splitDecision]. Edit as you see fit.",
  604. {splitDecision: DATASET_LABEL_MAP[splitDecision]}
  605. )}
  606. </Alert>
  607. );
  608. }
  609. return null;
  610. }
  611. renderTips() {
  612. const {tips} = this.state;
  613. if (tips) {
  614. return tips.map((tip, index) => (
  615. <Alert type="info" showIcon key={`tip-${index}`}>
  616. <TipContainer dangerouslySetInnerHTML={{__html: marked(tip)}} />
  617. </Alert>
  618. ));
  619. }
  620. return null;
  621. }
  622. setTips = (tips: string[]) => {
  623. // If there are currently no tips set and the new tips are empty, do nothing
  624. // and bail out of an expensive entire table rerender
  625. if (!tips.length && !this.state.tips.length) {
  626. return;
  627. }
  628. this.setState({tips});
  629. };
  630. setSplitDecision = (value?: SavedQueryDatasets) => {
  631. const {eventView} = this.state;
  632. const newEventView = eventView.withDataset(
  633. getDatasetFromLocationOrSavedQueryDataset(undefined, value)
  634. );
  635. this.setState({
  636. splitDecision: value,
  637. savedQueryDataset: value,
  638. eventView: newEventView,
  639. });
  640. };
  641. renderSearchBar(customMeasurements: CustomMeasurementCollection | undefined) {
  642. const {organization} = this.props;
  643. const {eventView} = this.state;
  644. const fields = eventView.hasAggregateField()
  645. ? generateAggregateFields(organization, eventView.fields)
  646. : eventView.fields;
  647. let savedSearchType: SavedSearchType | undefined = SavedSearchType.EVENT;
  648. if (hasDatasetSelector(organization)) {
  649. savedSearchType =
  650. eventView.dataset === DiscoverDatasets.TRANSACTIONS
  651. ? SavedSearchType.TRANSACTION
  652. : SavedSearchType.ERROR;
  653. }
  654. return (
  655. <Wrapper>
  656. <ResultsSearchQueryBuilder
  657. projectIds={eventView.project}
  658. query={eventView.query}
  659. fields={fields}
  660. onSearch={this.handleSearch}
  661. customMeasurements={customMeasurements}
  662. dataset={eventView.dataset}
  663. includeTransactions
  664. recentSearches={savedSearchType}
  665. />
  666. </Wrapper>
  667. );
  668. }
  669. render() {
  670. const {organization, location, router, selection, api, setSavedQuery, isHomepage} =
  671. this.props;
  672. const {
  673. eventView,
  674. error,
  675. errorCode,
  676. totalValues,
  677. showTags,
  678. confirmedQuery,
  679. savedQuery,
  680. splitDecision,
  681. savedQueryDataset,
  682. } = this.state;
  683. const hasDatasetSelectorFeature = hasDatasetSelector(organization);
  684. const query = eventView.query;
  685. const title = this.getDocumentTitle();
  686. const yAxisArray = getYAxis(location, eventView, savedQuery);
  687. if (!eventView.isValid()) {
  688. return <LoadingIndicator />;
  689. }
  690. return (
  691. <SentryDocumentTitle title={title} orgSlug={organization.slug}>
  692. <Fragment>
  693. <ResultsHeader
  694. setSavedQuery={setSavedQuery}
  695. errorCode={errorCode}
  696. organization={organization}
  697. location={location}
  698. eventView={eventView}
  699. yAxis={yAxisArray}
  700. router={router}
  701. isHomepage={isHomepage}
  702. splitDecision={splitDecision}
  703. />
  704. <Layout.Body>
  705. <CustomMeasurementsProvider organization={organization} selection={selection}>
  706. <Top fullWidth>
  707. {this.renderMetricsFallbackBanner()}
  708. {this.renderError(error)}
  709. {this.renderTips()}
  710. {this.renderForcedDatasetBanner()}
  711. {this.renderQueryIncompatibleWithDatasetBanner()}
  712. {!hasDatasetSelectorFeature && <SampleDataAlert query={query} />}
  713. <Wrapper>
  714. <PageFilterBar condensed>
  715. <ProjectPageFilter />
  716. <EnvironmentPageFilter />
  717. <DatePageFilter />
  718. </PageFilterBar>
  719. </Wrapper>
  720. <CustomMeasurementsContext.Consumer>
  721. {contextValue =>
  722. this.renderSearchBar(contextValue?.customMeasurements ?? undefined)
  723. }
  724. </CustomMeasurementsContext.Consumer>
  725. <MetricsCardinalityProvider
  726. organization={organization}
  727. location={location}
  728. >
  729. <ResultsChart
  730. api={api}
  731. router={router}
  732. organization={organization}
  733. eventView={eventView}
  734. location={location}
  735. onAxisChange={this.handleYAxisChange}
  736. onDisplayChange={this.handleDisplayChange}
  737. onTopEventsChange={this.handleTopEventsChange}
  738. onIntervalChange={this.handleIntervalChange}
  739. total={totalValues}
  740. confirmedQuery={confirmedQuery}
  741. yAxis={yAxisArray}
  742. />
  743. </MetricsCardinalityProvider>
  744. </Top>
  745. <Layout.Main fullWidth={!showTags}>
  746. <Table
  747. organization={organization}
  748. eventView={eventView}
  749. location={location}
  750. title={title}
  751. setError={this.setError}
  752. onChangeShowTags={this.handleChangeShowTags}
  753. showTags={showTags}
  754. confirmedQuery={confirmedQuery}
  755. onCursor={this.handleCursor}
  756. isHomepage={isHomepage}
  757. setTips={this.setTips}
  758. queryDataset={savedQueryDataset}
  759. setSplitDecision={(value?: SavedQueryDatasets) => {
  760. if (
  761. hasDatasetSelectorFeature &&
  762. value !== SavedQueryDatasets.DISCOVER &&
  763. value !== savedQuery?.dataset
  764. ) {
  765. this.setSplitDecision(value);
  766. }
  767. }}
  768. dataset={hasDatasetSelectorFeature ? eventView.dataset : undefined}
  769. />
  770. </Layout.Main>
  771. {showTags ? this.renderTagsTable() : null}
  772. <Confirm
  773. priority="primary"
  774. header={<strong>{t('May lead to thumb twiddling')}</strong>}
  775. confirmText={t('Do it')}
  776. cancelText={t('Nevermind')}
  777. onConfirm={this.handleConfirmed}
  778. onCancel={this.handleCancelled}
  779. message={
  780. <p>
  781. {tct(
  782. `You've created a query that will search for events made
  783. [dayLimit:over more than 30 days] for [projectLimit:more than 10 projects].
  784. A lot has happened during that time, so this might take awhile.
  785. Are you sure you want to do this?`,
  786. {
  787. dayLimit: <strong />,
  788. projectLimit: <strong />,
  789. }
  790. )}
  791. </p>
  792. }
  793. >
  794. {this.setOpenFunction}
  795. </Confirm>
  796. </CustomMeasurementsProvider>
  797. </Layout.Body>
  798. </Fragment>
  799. </SentryDocumentTitle>
  800. );
  801. }
  802. }
  803. const Wrapper = styled('div')`
  804. display: flex;
  805. flex-direction: row;
  806. gap: ${space(1)};
  807. margin-bottom: ${space(2)};
  808. @media (max-width: ${p => p.theme.breakpoints.small}) {
  809. display: grid;
  810. grid-auto-flow: row;
  811. }
  812. `;
  813. const Top = styled(Layout.Main)`
  814. flex-grow: 0;
  815. `;
  816. const TipContainer = styled('span')`
  817. > p {
  818. margin: 0;
  819. }
  820. `;
  821. type SavedQueryState = DeprecatedAsyncComponent['state'] & {
  822. savedQuery?: SavedQuery | null;
  823. };
  824. class SavedQueryAPI extends DeprecatedAsyncComponent<Props, SavedQueryState> {
  825. shouldReload = true;
  826. getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
  827. const {organization, location} = this.props;
  828. const endpoints: ReturnType<DeprecatedAsyncComponent['getEndpoints']> = [];
  829. if (location.query.id) {
  830. endpoints.push([
  831. 'savedQuery',
  832. `/organizations/${organization.slug}/discover/saved/${location.query.id}/`,
  833. ]);
  834. return endpoints;
  835. }
  836. return endpoints;
  837. }
  838. setSavedQuery = (newSavedQuery?: SavedQuery) => {
  839. this.setState({savedQuery: newSavedQuery});
  840. };
  841. renderBody(): React.ReactNode {
  842. const {organization} = this.props;
  843. const {savedQuery, loading} = this.state;
  844. let savedQueryWithDataset = savedQuery;
  845. if (hasDatasetSelector(organization) && savedQuery) {
  846. savedQueryWithDataset = getSavedQueryWithDataset(savedQuery) as SavedQuery;
  847. }
  848. return (
  849. <Results
  850. {...this.props}
  851. savedQuery={savedQueryWithDataset ?? undefined}
  852. loading={loading}
  853. setSavedQuery={this.setSavedQuery}
  854. />
  855. );
  856. }
  857. }
  858. function ResultsContainer(props: Props) {
  859. /**
  860. * Block `<Results>` from mounting until GSH is ready since there are API
  861. * requests being performed on mount.
  862. *
  863. * Also, we skip loading last used projects if you have multiple projects feature as
  864. * you no longer need to enforce a project if it is empty. We assume an empty project is
  865. * the desired behavior because saved queries can contain a project filter. The only
  866. * exception is if we are showing a prebuilt saved query in which case we want to
  867. * respect pinned filters.
  868. */
  869. return (
  870. <PageFiltersContainer
  871. disablePersistence={
  872. props.organization.features.includes('discover-query') &&
  873. !!(props.savedQuery || props.location.query.id)
  874. }
  875. skipLoadLastUsed={
  876. props.organization.features.includes('global-views') && !!props.savedQuery
  877. }
  878. // The Discover Results component will manage URL params, including page filters state
  879. // This avoids an unnecessary re-render when forcing a project filter for team plan users
  880. skipInitializeUrlParams
  881. >
  882. <SavedQueryAPI {...props} />
  883. </PageFiltersContainer>
  884. );
  885. }
  886. export default withApi(withOrganization(withPageFilters(ResultsContainer)));
  887. const StyledCloseButton = styled(Button)`
  888. background-color: transparent;
  889. transition: opacity 0.1s linear;
  890. &:hover,
  891. &:focus {
  892. background-color: transparent;
  893. opacity: 1;
  894. }
  895. `;