results.tsx 25 KB

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