transactionsList.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import {Component, Fragment} 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/eventsV2/table/cellAction';
  20. import {TableColumn} from 'sentry/views/eventsV2/table/types';
  21. import {decodeColumnOrder} from 'sentry/views/eventsV2/utils';
  22. import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
  23. import {mapShowTransactionToPercentile} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
  24. import {TransactionFilterOptions} from 'sentry/views/performance/transactionSummary/utils';
  25. import {TrendChangeType, TrendView} from 'sentry/views/performance/trends/types';
  26. import TransactionsTable from './transactionsTable';
  27. const DEFAULT_TRANSACTION_LIMIT = 5;
  28. export type DropdownOption = {
  29. /**
  30. * The label to display in the dropdown
  31. */
  32. label: string;
  33. /**
  34. * The sort to apply to the eventView when this is selected.
  35. */
  36. sort: Sort;
  37. /**
  38. * The unique name to use for this option.
  39. */
  40. value: string;
  41. /**
  42. * override the eventView query
  43. */
  44. query?: [string, string][];
  45. /**
  46. * Included if the option is for a trend
  47. */
  48. trendType?: TrendChangeType;
  49. };
  50. type Props = {
  51. /**
  52. * The name of the url parameter that contains the cursor info.
  53. */
  54. cursorName: string;
  55. eventView: EventView;
  56. /**
  57. * The callback for when the dropdown option changes.
  58. */
  59. handleDropdownChange: (k: string) => void;
  60. /**
  61. * The limit to the number of results to fetch.
  62. */
  63. limit: number;
  64. location: Location;
  65. /**
  66. * The available options for the dropdown.
  67. */
  68. options: DropdownOption[];
  69. organization: Organization;
  70. /**
  71. * The currently selected option on the dropdown.
  72. */
  73. selected: DropdownOption;
  74. breakdown?: SpanOperationBreakdownFilter;
  75. /**
  76. * Show a loading indicator instead of the table, used for transaction summary p95.
  77. */
  78. forceLoading?: boolean;
  79. /**
  80. * Optional callback function to generate an alternative EventView object to be used
  81. * for generating the Discover query.
  82. */
  83. generateDiscoverEventView?: () => EventView;
  84. /**
  85. * A map of callbacks to generate a link for a column based on the title.
  86. */
  87. generateLink?: Record<
  88. string,
  89. (
  90. organization: Organization,
  91. tableRow: TableDataRow,
  92. query: Query
  93. ) => LocationDescriptor
  94. >;
  95. generatePerformanceTransactionEventsView?: () => EventView;
  96. /**
  97. * The callback to generate a cell action handler for a column
  98. */
  99. handleCellAction?: (
  100. c: TableColumn<React.ReactText>
  101. ) => (a: Actions, v: React.ReactText) => void;
  102. /**
  103. * The callback for when View All Events is clicked.
  104. */
  105. handleOpenAllEventsClick?: (e: React.MouseEvent<Element>) => void;
  106. /**
  107. * The callback for when Open in Discover is clicked.
  108. */
  109. handleOpenInDiscoverClick?: (e: React.MouseEvent<Element>) => void;
  110. showTransactions?: TransactionFilterOptions;
  111. /**
  112. * A list of preferred table headers to use over the field names.
  113. */
  114. titles?: string[];
  115. trendView?: TrendView;
  116. };
  117. class _TransactionsList extends Component<Props> {
  118. static defaultProps = {
  119. cursorName: 'transactionCursor',
  120. limit: DEFAULT_TRANSACTION_LIMIT,
  121. };
  122. handleCursor: CursorHandler = (cursor, pathname, query) => {
  123. const {cursorName} = this.props;
  124. browserHistory.push({
  125. pathname,
  126. query: {...query, [cursorName]: cursor},
  127. });
  128. };
  129. getEventView() {
  130. const {eventView, selected} = this.props;
  131. const sortedEventView = eventView.withSorts([selected.sort]);
  132. if (selected.query) {
  133. const query = new MutableSearch(sortedEventView.query);
  134. selected.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
  135. sortedEventView.query = query.formatString();
  136. }
  137. return sortedEventView;
  138. }
  139. generateDiscoverEventView(): EventView {
  140. const {generateDiscoverEventView} = this.props;
  141. if (typeof generateDiscoverEventView === 'function') {
  142. return generateDiscoverEventView();
  143. }
  144. return this.getEventView();
  145. }
  146. generatePerformanceTransactionEventsView(): EventView {
  147. const {generatePerformanceTransactionEventsView} = this.props;
  148. return generatePerformanceTransactionEventsView?.() ?? this.getEventView();
  149. }
  150. renderHeader(): React.ReactNode {
  151. const {
  152. organization,
  153. selected,
  154. options,
  155. handleDropdownChange,
  156. handleOpenAllEventsClick,
  157. handleOpenInDiscoverClick,
  158. showTransactions,
  159. breakdown,
  160. } = this.props;
  161. return (
  162. <Fragment>
  163. <div>
  164. <CompactSelect
  165. triggerProps={{prefix: t('Filter'), size: 'xs'}}
  166. value={selected.value}
  167. options={options}
  168. onChange={opt => handleDropdownChange(opt.value)}
  169. />
  170. </div>
  171. {!this.isTrend() &&
  172. (handleOpenAllEventsClick ? (
  173. <GuideAnchor target="release_transactions_open_in_transaction_events">
  174. <Button
  175. onClick={handleOpenAllEventsClick}
  176. to={this.generatePerformanceTransactionEventsView().getPerformanceTransactionEventsViewUrlTarget(
  177. organization.slug,
  178. {
  179. showTransactions: mapShowTransactionToPercentile(showTransactions),
  180. breakdown,
  181. }
  182. )}
  183. size="xs"
  184. data-test-id="transaction-events-open"
  185. >
  186. {t('View All Events')}
  187. </Button>
  188. </GuideAnchor>
  189. ) : (
  190. <GuideAnchor target="release_transactions_open_in_discover">
  191. <DiscoverButton
  192. onClick={handleOpenInDiscoverClick}
  193. to={this.generateDiscoverEventView().getResultsViewUrlTarget(
  194. organization.slug
  195. )}
  196. size="xs"
  197. data-test-id="discover-open"
  198. >
  199. {t('Open in Discover')}
  200. </DiscoverButton>
  201. </GuideAnchor>
  202. ))}
  203. </Fragment>
  204. );
  205. }
  206. renderTransactionTable(): React.ReactNode {
  207. const {
  208. location,
  209. organization,
  210. handleCellAction,
  211. cursorName,
  212. limit,
  213. titles,
  214. generateLink,
  215. forceLoading,
  216. } = this.props;
  217. const useEvents = organization.features.includes(
  218. 'performance-frontend-use-events-endpoint'
  219. );
  220. const eventView = this.getEventView();
  221. const columnOrder = eventView.getColumns();
  222. const cursor = decodeScalar(location.query?.[cursorName]);
  223. const tableRenderer = ({isLoading, pageLinks, tableData}) => (
  224. <Fragment>
  225. <Header>
  226. {this.renderHeader()}
  227. <StyledPagination
  228. pageLinks={pageLinks}
  229. onCursor={this.handleCursor}
  230. size="xs"
  231. />
  232. </Header>
  233. <GuideAnchor target="transactions_table" position="top-start">
  234. <TransactionsTable
  235. eventView={eventView}
  236. organization={organization}
  237. location={location}
  238. isLoading={isLoading}
  239. tableData={tableData}
  240. columnOrder={columnOrder}
  241. titles={titles}
  242. generateLink={generateLink}
  243. handleCellAction={handleCellAction}
  244. useAggregateAlias={!useEvents}
  245. />
  246. </GuideAnchor>
  247. </Fragment>
  248. );
  249. if (forceLoading) {
  250. return tableRenderer({
  251. isLoading: true,
  252. pageLinks: null,
  253. tableData: null,
  254. });
  255. }
  256. return (
  257. <DiscoverQuery
  258. location={location}
  259. eventView={eventView}
  260. orgSlug={organization.slug}
  261. limit={limit}
  262. cursor={cursor}
  263. referrer="api.discover.transactions-list"
  264. useEvents={useEvents}
  265. >
  266. {tableRenderer}
  267. </DiscoverQuery>
  268. );
  269. }
  270. renderTrendsTable(): React.ReactNode {
  271. const {trendView, location, selected, organization, cursorName, generateLink} =
  272. this.props;
  273. const sortedEventView: TrendView = trendView!.clone();
  274. sortedEventView.sorts = [selected.sort];
  275. sortedEventView.trendType = selected.trendType;
  276. if (selected.query) {
  277. const query = new MutableSearch(sortedEventView.query);
  278. selected.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
  279. sortedEventView.query = query.formatString();
  280. }
  281. const cursor = decodeScalar(location.query?.[cursorName]);
  282. return (
  283. <TrendsEventsDiscoverQuery
  284. eventView={sortedEventView}
  285. orgSlug={organization.slug}
  286. location={location}
  287. cursor={cursor}
  288. limit={5}
  289. >
  290. {({isLoading, trendsData, pageLinks}) => (
  291. <Fragment>
  292. <Header>
  293. {this.renderHeader()}
  294. <StyledPagination
  295. pageLinks={pageLinks}
  296. onCursor={this.handleCursor}
  297. size="sm"
  298. />
  299. </Header>
  300. <TransactionsTable
  301. eventView={sortedEventView}
  302. organization={organization}
  303. location={location}
  304. isLoading={isLoading}
  305. tableData={trendsData}
  306. titles={['transaction', 'percentage', 'difference']}
  307. columnOrder={decodeColumnOrder([
  308. {field: 'transaction'},
  309. {field: 'trend_percentage()'},
  310. {field: 'trend_difference()'},
  311. ])}
  312. generateLink={generateLink}
  313. useAggregateAlias
  314. />
  315. </Fragment>
  316. )}
  317. </TrendsEventsDiscoverQuery>
  318. );
  319. }
  320. isTrend(): boolean {
  321. const {selected} = this.props;
  322. return selected.trendType !== undefined;
  323. }
  324. render() {
  325. return (
  326. <Fragment>
  327. {this.isTrend() ? this.renderTrendsTable() : this.renderTransactionTable()}
  328. </Fragment>
  329. );
  330. }
  331. }
  332. const Header = styled('div')`
  333. display: grid;
  334. grid-template-columns: 1fr auto auto;
  335. margin-bottom: ${space(1)};
  336. align-items: center;
  337. `;
  338. const StyledPagination = styled(Pagination)`
  339. margin: 0 0 0 ${space(1)};
  340. `;
  341. const TransactionsList = (
  342. props: Omit<Props, 'cursorName' | 'limit'> & {
  343. cursorName?: Props['cursorName'];
  344. limit?: Props['limit'];
  345. }
  346. ) => {
  347. return <_TransactionsList {...props} />;
  348. };
  349. export default TransactionsList;