transactionsList.tsx 15 KB

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