results.tsx 28 KB

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