transactionsList.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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 DiscoverButton from 'sentry/components/discoverButton';
  8. import CompactSelect from 'sentry/components/forms/compactSelect';
  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. <TransactionsTable
  234. eventView={eventView}
  235. organization={organization}
  236. location={location}
  237. isLoading={isLoading}
  238. tableData={tableData}
  239. columnOrder={columnOrder}
  240. titles={titles}
  241. generateLink={generateLink}
  242. handleCellAction={handleCellAction}
  243. useAggregateAlias={!useEvents}
  244. />
  245. </Fragment>
  246. );
  247. if (forceLoading) {
  248. return tableRenderer({
  249. isLoading: true,
  250. pageLinks: null,
  251. tableData: null,
  252. });
  253. }
  254. return (
  255. <DiscoverQuery
  256. location={location}
  257. eventView={eventView}
  258. orgSlug={organization.slug}
  259. limit={limit}
  260. cursor={cursor}
  261. referrer="api.discover.transactions-list"
  262. useEvents={useEvents}
  263. >
  264. {tableRenderer}
  265. </DiscoverQuery>
  266. );
  267. }
  268. renderTrendsTable(): React.ReactNode {
  269. const {trendView, location, selected, organization, cursorName, generateLink} =
  270. this.props;
  271. const sortedEventView: TrendView = trendView!.clone();
  272. sortedEventView.sorts = [selected.sort];
  273. sortedEventView.trendType = selected.trendType;
  274. if (selected.query) {
  275. const query = new MutableSearch(sortedEventView.query);
  276. selected.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
  277. sortedEventView.query = query.formatString();
  278. }
  279. const cursor = decodeScalar(location.query?.[cursorName]);
  280. return (
  281. <TrendsEventsDiscoverQuery
  282. eventView={sortedEventView}
  283. orgSlug={organization.slug}
  284. location={location}
  285. cursor={cursor}
  286. limit={5}
  287. >
  288. {({isLoading, trendsData, pageLinks}) => (
  289. <Fragment>
  290. <Header>
  291. {this.renderHeader()}
  292. <StyledPagination
  293. pageLinks={pageLinks}
  294. onCursor={this.handleCursor}
  295. size="sm"
  296. />
  297. </Header>
  298. <TransactionsTable
  299. eventView={sortedEventView}
  300. organization={organization}
  301. location={location}
  302. isLoading={isLoading}
  303. tableData={trendsData}
  304. titles={['transaction', 'percentage', 'difference']}
  305. columnOrder={decodeColumnOrder([
  306. {field: 'transaction'},
  307. {field: 'trend_percentage()'},
  308. {field: 'trend_difference()'},
  309. ])}
  310. generateLink={generateLink}
  311. useAggregateAlias
  312. />
  313. </Fragment>
  314. )}
  315. </TrendsEventsDiscoverQuery>
  316. );
  317. }
  318. isTrend(): boolean {
  319. const {selected} = this.props;
  320. return selected.trendType !== undefined;
  321. }
  322. render() {
  323. return (
  324. <Fragment>
  325. {this.isTrend() ? this.renderTrendsTable() : this.renderTransactionTable()}
  326. </Fragment>
  327. );
  328. }
  329. }
  330. const Header = styled('div')`
  331. display: grid;
  332. grid-template-columns: 1fr auto auto;
  333. margin-bottom: ${space(1)};
  334. align-items: center;
  335. `;
  336. const StyledPagination = styled(Pagination)`
  337. margin: 0 0 0 ${space(1)};
  338. `;
  339. const TransactionsList = (
  340. props: Omit<Props, 'cursorName' | 'limit'> & {
  341. cursorName?: Props['cursorName'];
  342. limit?: Props['limit'];
  343. }
  344. ) => {
  345. return <_TransactionsList {...props} />;
  346. };
  347. export default TransactionsList;