results.tsx 24 KB

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