transactionsList.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import {Component, Fragment, useContext, useEffect} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location, LocationDescriptor, Query} from 'history';
  5. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  6. import {Button} from 'sentry/components/button';
  7. import {CompactSelect} from 'sentry/components/compactSelect';
  8. import DiscoverButton from 'sentry/components/discoverButton';
  9. import Pagination, {CursorHandler} from 'sentry/components/pagination';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {Organization} from 'sentry/types';
  13. import DiscoverQuery, {TableDataRow} from 'sentry/utils/discover/discoverQuery';
  14. import EventView from 'sentry/utils/discover/eventView';
  15. import {Sort} from 'sentry/utils/discover/fields';
  16. import {TrendsEventsDiscoverQuery} from 'sentry/utils/performance/trends/trendsDiscoverQuery';
  17. import {decodeScalar} from 'sentry/utils/queryString';
  18. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  19. import {Actions} from 'sentry/views/discover/table/cellAction';
  20. import {TableColumn} from 'sentry/views/discover/table/types';
  21. import {decodeColumnOrder} from 'sentry/views/discover/utils';
  22. import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
  23. import {mapShowTransactionToPercentile} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
  24. import {PerformanceAtScaleContext} from 'sentry/views/performance/transactionSummary/transactionOverview/performanceAtScaleContext';
  25. import {
  26. DisplayModes,
  27. TransactionFilterOptions,
  28. } from 'sentry/views/performance/transactionSummary/utils';
  29. import {TrendChangeType, TrendView} from 'sentry/views/performance/trends/types';
  30. import TransactionsTable from './transactionsTable';
  31. const DEFAULT_TRANSACTION_LIMIT = 5;
  32. export type DropdownOption = {
  33. /**
  34. * The label to display in the dropdown
  35. */
  36. label: string;
  37. /**
  38. * The sort to apply to the eventView when this is selected.
  39. */
  40. sort: Sort;
  41. /**
  42. * The unique name to use for this option.
  43. */
  44. value: string;
  45. /**
  46. * override the eventView query
  47. */
  48. query?: [string, string][];
  49. /**
  50. * Included if the option is for a trend
  51. */
  52. trendType?: TrendChangeType;
  53. };
  54. type Props = {
  55. /**
  56. * The name of the url parameter that contains the cursor info.
  57. */
  58. cursorName: string;
  59. eventView: EventView;
  60. /**
  61. * The callback for when the dropdown option changes.
  62. */
  63. handleDropdownChange: (k: string) => void;
  64. /**
  65. * The limit to the number of results to fetch.
  66. */
  67. limit: number;
  68. location: Location;
  69. /**
  70. * The available options for the dropdown.
  71. */
  72. options: DropdownOption[];
  73. organization: Organization;
  74. /**
  75. * The currently selected option on the dropdown.
  76. */
  77. selected: DropdownOption;
  78. breakdown?: SpanOperationBreakdownFilter;
  79. /**
  80. * Show a loading indicator instead of the table, used for transaction summary p95.
  81. */
  82. forceLoading?: boolean;
  83. /**
  84. * Optional callback function to generate an alternative EventView object to be used
  85. * for generating the Discover query.
  86. */
  87. generateDiscoverEventView?: () => EventView;
  88. /**
  89. * A map of callbacks to generate a link for a column based on the title.
  90. */
  91. generateLink?: Record<
  92. string,
  93. (
  94. organization: Organization,
  95. tableRow: TableDataRow,
  96. query: Query
  97. ) => LocationDescriptor
  98. >;
  99. generatePerformanceTransactionEventsView?: () => EventView;
  100. /**
  101. * The callback to generate a cell action handler for a column
  102. */
  103. handleCellAction?: (
  104. c: TableColumn<React.ReactText>
  105. ) => (a: Actions, v: React.ReactText) => void;
  106. /**
  107. * The callback for when View All Events is clicked.
  108. */
  109. handleOpenAllEventsClick?: (e: React.MouseEvent<Element>) => void;
  110. /**
  111. * The callback for when Open in Discover is clicked.
  112. */
  113. handleOpenInDiscoverClick?: (e: React.MouseEvent<Element>) => void;
  114. referrer?: string;
  115. showTransactions?: TransactionFilterOptions;
  116. /**
  117. * A list of preferred table headers to use over the field names.
  118. */
  119. titles?: string[];
  120. trendView?: TrendView;
  121. };
  122. type TableRenderProps = Omit<React.ComponentProps<typeof Pagination>, 'size'> &
  123. React.ComponentProps<typeof TransactionsTable> & {
  124. header: React.ReactNode;
  125. paginationCursorSize: React.ComponentProps<typeof Pagination>['size'];
  126. target?: string;
  127. };
  128. function TableRender({
  129. pageLinks,
  130. onCursor,
  131. header,
  132. eventView,
  133. organization,
  134. isLoading,
  135. location,
  136. columnOrder,
  137. tableData,
  138. titles,
  139. generateLink,
  140. handleCellAction,
  141. referrer,
  142. useAggregateAlias,
  143. target,
  144. paginationCursorSize,
  145. }: TableRenderProps) {
  146. const query = decodeScalar(location.query.query, '');
  147. const display = decodeScalar(location.query.display, DisplayModes.DURATION);
  148. const performanceAtScaleContext = useContext(PerformanceAtScaleContext);
  149. const hasResults =
  150. tableData && tableData.data && tableData.meta && tableData.data.length > 0;
  151. useEffect(() => {
  152. if (!performanceAtScaleContext) {
  153. return;
  154. }
  155. // we are now only collecting analytics data from the transaction summary page
  156. // when the display mode is set to duration
  157. if (display !== DisplayModes.DURATION) {
  158. return;
  159. }
  160. if (isLoading || hasResults === null) {
  161. performanceAtScaleContext.setTransactionListTableData(undefined);
  162. return;
  163. }
  164. if (
  165. !hasResults === performanceAtScaleContext.transactionListTableData?.empty &&
  166. query === performanceAtScaleContext.transactionListTableData?.query
  167. ) {
  168. return;
  169. }
  170. performanceAtScaleContext.setTransactionListTableData({
  171. empty: !hasResults,
  172. query,
  173. });
  174. }, [display, isLoading, hasResults, performanceAtScaleContext, query]);
  175. const content = (
  176. <TransactionsTable
  177. eventView={eventView}
  178. organization={organization}
  179. location={location}
  180. isLoading={isLoading}
  181. tableData={tableData}
  182. columnOrder={columnOrder}
  183. titles={titles}
  184. generateLink={generateLink}
  185. handleCellAction={handleCellAction}
  186. useAggregateAlias={useAggregateAlias}
  187. referrer={referrer}
  188. />
  189. );
  190. return (
  191. <Fragment>
  192. <Header>
  193. {header}
  194. <StyledPagination
  195. pageLinks={pageLinks}
  196. onCursor={onCursor}
  197. size={paginationCursorSize}
  198. />
  199. </Header>
  200. {target ? (
  201. <GuideAnchor target={target} position="top-start">
  202. {content}
  203. </GuideAnchor>
  204. ) : (
  205. content
  206. )}
  207. </Fragment>
  208. );
  209. }
  210. class _TransactionsList extends Component<Props> {
  211. static defaultProps = {
  212. cursorName: 'transactionCursor',
  213. limit: DEFAULT_TRANSACTION_LIMIT,
  214. };
  215. handleCursor: CursorHandler = (cursor, pathname, query) => {
  216. const {cursorName} = this.props;
  217. browserHistory.push({
  218. pathname,
  219. query: {...query, [cursorName]: cursor},
  220. });
  221. };
  222. getEventView() {
  223. const {eventView, selected} = this.props;
  224. const sortedEventView = eventView.withSorts([selected.sort]);
  225. if (selected.query) {
  226. const query = new MutableSearch(sortedEventView.query);
  227. selected.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
  228. sortedEventView.query = query.formatString();
  229. }
  230. return sortedEventView;
  231. }
  232. generateDiscoverEventView(): EventView {
  233. const {generateDiscoverEventView} = this.props;
  234. if (typeof generateDiscoverEventView === 'function') {
  235. return generateDiscoverEventView();
  236. }
  237. return this.getEventView();
  238. }
  239. generatePerformanceTransactionEventsView(): EventView {
  240. const {generatePerformanceTransactionEventsView} = this.props;
  241. return generatePerformanceTransactionEventsView?.() ?? this.getEventView();
  242. }
  243. renderHeader(): React.ReactNode {
  244. const {
  245. organization,
  246. selected,
  247. options,
  248. handleDropdownChange,
  249. handleOpenAllEventsClick,
  250. handleOpenInDiscoverClick,
  251. showTransactions,
  252. breakdown,
  253. } = this.props;
  254. return (
  255. <Fragment>
  256. <div>
  257. <CompactSelect
  258. triggerProps={{prefix: t('Filter'), size: 'xs'}}
  259. value={selected.value}
  260. options={options}
  261. onChange={opt => handleDropdownChange(opt.value)}
  262. />
  263. </div>
  264. {!this.isTrend() &&
  265. (handleOpenAllEventsClick ? (
  266. <GuideAnchor target="release_transactions_open_in_transaction_events">
  267. <Button
  268. onClick={handleOpenAllEventsClick}
  269. to={this.generatePerformanceTransactionEventsView().getPerformanceTransactionEventsViewUrlTarget(
  270. organization.slug,
  271. {
  272. showTransactions: mapShowTransactionToPercentile(showTransactions),
  273. breakdown,
  274. }
  275. )}
  276. size="xs"
  277. data-test-id="transaction-events-open"
  278. >
  279. {t('View All Events')}
  280. </Button>
  281. </GuideAnchor>
  282. ) : (
  283. <GuideAnchor target="release_transactions_open_in_discover">
  284. <DiscoverButton
  285. onClick={handleOpenInDiscoverClick}
  286. to={this.generateDiscoverEventView().getResultsViewUrlTarget(
  287. organization.slug
  288. )}
  289. size="xs"
  290. data-test-id="discover-open"
  291. >
  292. {t('Open in Discover')}
  293. </DiscoverButton>
  294. </GuideAnchor>
  295. ))}
  296. </Fragment>
  297. );
  298. }
  299. renderTransactionTable(): React.ReactNode {
  300. const {
  301. location,
  302. organization,
  303. handleCellAction,
  304. cursorName,
  305. limit,
  306. titles,
  307. generateLink,
  308. forceLoading,
  309. referrer,
  310. } = this.props;
  311. const eventView = this.getEventView();
  312. const columnOrder = eventView.getColumns();
  313. const cursor = decodeScalar(location.query?.[cursorName]);
  314. const tableCommonProps: Omit<
  315. TableRenderProps,
  316. 'isLoading' | 'pageLinks' | 'tableData'
  317. > = {
  318. handleCellAction,
  319. referrer,
  320. eventView,
  321. organization,
  322. location,
  323. columnOrder,
  324. titles,
  325. generateLink,
  326. useAggregateAlias: false,
  327. header: this.renderHeader(),
  328. target: 'transactions_table',
  329. paginationCursorSize: 'xs',
  330. onCursor: this.handleCursor,
  331. };
  332. if (forceLoading) {
  333. return (
  334. <TableRender {...tableCommonProps} isLoading pageLinks={null} tableData={null} />
  335. );
  336. }
  337. return (
  338. <DiscoverQuery
  339. location={location}
  340. eventView={eventView}
  341. orgSlug={organization.slug}
  342. limit={limit}
  343. cursor={cursor}
  344. referrer="api.discover.transactions-list"
  345. >
  346. {({isLoading, pageLinks, tableData}) => (
  347. <TableRender
  348. {...tableCommonProps}
  349. isLoading={isLoading}
  350. pageLinks={pageLinks}
  351. tableData={tableData}
  352. />
  353. )}
  354. </DiscoverQuery>
  355. );
  356. }
  357. renderTrendsTable(): React.ReactNode {
  358. const {trendView, location, selected, organization, cursorName, generateLink} =
  359. this.props;
  360. const sortedEventView: TrendView = trendView!.clone();
  361. sortedEventView.sorts = [selected.sort];
  362. sortedEventView.trendType = selected.trendType;
  363. if (selected.query) {
  364. const query = new MutableSearch(sortedEventView.query);
  365. selected.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
  366. sortedEventView.query = query.formatString();
  367. }
  368. const cursor = decodeScalar(location.query?.[cursorName]);
  369. return (
  370. <TrendsEventsDiscoverQuery
  371. eventView={sortedEventView}
  372. orgSlug={organization.slug}
  373. location={location}
  374. cursor={cursor}
  375. limit={5}
  376. >
  377. {({isLoading, trendsData, pageLinks}) => (
  378. <TableRender
  379. organization={organization}
  380. eventView={sortedEventView}
  381. location={location}
  382. isLoading={isLoading}
  383. tableData={trendsData}
  384. pageLinks={pageLinks}
  385. onCursor={this.handleCursor}
  386. paginationCursorSize="sm"
  387. header={this.renderHeader()}
  388. titles={['transaction', 'percentage', 'difference']}
  389. columnOrder={decodeColumnOrder([
  390. {field: 'transaction'},
  391. {field: 'trend_percentage()'},
  392. {field: 'trend_difference()'},
  393. ])}
  394. generateLink={generateLink}
  395. useAggregateAlias
  396. />
  397. )}
  398. </TrendsEventsDiscoverQuery>
  399. );
  400. }
  401. isTrend(): boolean {
  402. const {selected} = this.props;
  403. return selected.trendType !== undefined;
  404. }
  405. render() {
  406. return (
  407. <Fragment>
  408. {this.isTrend() ? this.renderTrendsTable() : this.renderTransactionTable()}
  409. </Fragment>
  410. );
  411. }
  412. }
  413. const Header = styled('div')`
  414. display: grid;
  415. grid-template-columns: 1fr auto auto;
  416. margin-bottom: ${space(1)};
  417. align-items: center;
  418. `;
  419. const StyledPagination = styled(Pagination)`
  420. margin: 0 0 0 ${space(1)};
  421. `;
  422. function TransactionsList(
  423. props: Omit<Props, 'cursorName' | 'limit'> & {
  424. cursorName?: Props['cursorName'];
  425. limit?: Props['limit'];
  426. }
  427. ) {
  428. return <_TransactionsList {...props} />;
  429. }
  430. export default TransactionsList;