transactionsList.tsx 11 KB

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