transactionsTable.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import {Fragment, PureComponent} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location, LocationDescriptor, Query} from 'history';
  4. import SortLink from 'sentry/components/gridEditable/sortLink';
  5. import Link from 'sentry/components/links/link';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import PanelTable from 'sentry/components/panels/panelTable';
  8. import QuestionTooltip from 'sentry/components/questionTooltip';
  9. import ReplayIdCountProvider from 'sentry/components/replays/replayIdCountProvider';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {Organization} from 'sentry/types';
  13. import {objectIsEmpty} from 'sentry/utils';
  14. import {trackAnalytics} from 'sentry/utils/analytics';
  15. import {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
  16. import EventView, {MetaType} from 'sentry/utils/discover/eventView';
  17. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  18. import {
  19. Alignments,
  20. fieldAlignment,
  21. getAggregateAlias,
  22. } from 'sentry/utils/discover/fields';
  23. import ViewReplayLink from 'sentry/utils/discover/viewReplayLink';
  24. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  25. import CellAction, {Actions} from 'sentry/views/discover/table/cellAction';
  26. import {TableColumn} from 'sentry/views/discover/table/types';
  27. import {GridCell, GridCellNumber} from 'sentry/views/performance/styles';
  28. import {TrendsDataEvents} from 'sentry/views/performance/trends/types';
  29. type Props = {
  30. columnOrder: TableColumn<React.ReactText>[];
  31. eventView: EventView;
  32. isLoading: boolean;
  33. location: Location;
  34. organization: Organization;
  35. tableData: TableData | TrendsDataEvents | null;
  36. useAggregateAlias: boolean;
  37. generateLink?: Record<
  38. string,
  39. (
  40. organization: Organization,
  41. tableRow: TableDataRow,
  42. query: Query
  43. ) => LocationDescriptor
  44. >;
  45. handleCellAction?: (
  46. c: TableColumn<React.ReactText>
  47. ) => (a: Actions, v: React.ReactText) => void;
  48. referrer?: string;
  49. titles?: string[];
  50. };
  51. class TransactionsTable extends PureComponent<Props> {
  52. getTitles() {
  53. const {eventView, titles} = this.props;
  54. return titles ?? eventView.getFields();
  55. }
  56. renderHeader() {
  57. const {tableData, columnOrder} = this.props;
  58. const tableMeta = tableData?.meta;
  59. const generateSortLink = () => undefined;
  60. const tableTitles = this.getTitles();
  61. const headers = tableTitles.map((title, index) => {
  62. const column = columnOrder[index];
  63. const align: Alignments = fieldAlignment(column.name, column.type, tableMeta);
  64. if (column.key === 'span_ops_breakdown.relative') {
  65. return (
  66. <HeadCellContainer key={index}>
  67. <SortLink
  68. align={align}
  69. title={
  70. title === t('operation duration') ? (
  71. <Fragment>
  72. {title}
  73. <StyledIconQuestion
  74. size="xs"
  75. position="top"
  76. title={t(
  77. `Span durations are summed over the course of an entire transaction. Any overlapping spans are only counted once.`
  78. )}
  79. />
  80. </Fragment>
  81. ) : (
  82. title
  83. )
  84. }
  85. direction={undefined}
  86. canSort={false}
  87. generateSortLink={generateSortLink}
  88. />
  89. </HeadCellContainer>
  90. );
  91. }
  92. return (
  93. <HeadCellContainer key={index}>
  94. <SortLink
  95. align={align}
  96. title={title}
  97. direction={undefined}
  98. canSort={false}
  99. generateSortLink={generateSortLink}
  100. />
  101. </HeadCellContainer>
  102. );
  103. });
  104. return headers;
  105. }
  106. renderRow(
  107. row: TableDataRow,
  108. rowIndex: number,
  109. columnOrder: TableColumn<React.ReactText>[],
  110. tableMeta: MetaType
  111. ): React.ReactNode[] {
  112. const {
  113. eventView,
  114. organization,
  115. location,
  116. generateLink,
  117. handleCellAction,
  118. titles,
  119. useAggregateAlias,
  120. referrer,
  121. } = this.props;
  122. const fields = eventView.getFields();
  123. if (titles && titles.length) {
  124. // Slice to match length of given titles
  125. columnOrder = columnOrder.slice(0, titles.length);
  126. }
  127. const resultsRow = columnOrder.map((column, index) => {
  128. const field = String(column.key);
  129. // TODO add a better abstraction for this in fieldRenderers.
  130. const fieldName = useAggregateAlias ? getAggregateAlias(field) : field;
  131. const fieldType = tableMeta[fieldName];
  132. const fieldRenderer = getFieldRenderer(field, tableMeta, useAggregateAlias);
  133. let rendered = fieldRenderer(row, {organization, location});
  134. const target = generateLink?.[field]?.(organization, row, location.query);
  135. if (target && !objectIsEmpty(target)) {
  136. if (fields[index] === 'replayId') {
  137. rendered = (
  138. <ViewReplayLink replayId={row.replayId} to={target}>
  139. {rendered}
  140. </ViewReplayLink>
  141. );
  142. } else if (fields[index] === 'profile.id') {
  143. rendered = (
  144. <Link
  145. data-test-id={`view-${fields[index]}`}
  146. to={target}
  147. onClick={getProfileAnalyticsHandler(organization, referrer)}
  148. >
  149. {rendered}
  150. </Link>
  151. );
  152. } else {
  153. rendered = (
  154. <Link data-test-id={`view-${fields[index]}`} to={target}>
  155. {rendered}
  156. </Link>
  157. );
  158. }
  159. }
  160. const isNumeric = ['integer', 'number', 'duration'].includes(fieldType);
  161. const key = `${rowIndex}:${column.key}:${index}`;
  162. rendered = isNumeric ? (
  163. <GridCellNumber data-test-id="grid-cell">{rendered}</GridCellNumber>
  164. ) : (
  165. <GridCell data-test-id="grid-cell">{rendered}</GridCell>
  166. );
  167. if (handleCellAction) {
  168. rendered = (
  169. <CellAction
  170. column={column}
  171. dataRow={row}
  172. handleCellAction={handleCellAction(column)}
  173. >
  174. {rendered}
  175. </CellAction>
  176. );
  177. }
  178. return <BodyCellContainer key={key}>{rendered}</BodyCellContainer>;
  179. });
  180. return resultsRow;
  181. }
  182. renderResults() {
  183. const {isLoading, tableData, columnOrder} = this.props;
  184. let cells: React.ReactNode[] = [];
  185. if (isLoading) {
  186. return cells;
  187. }
  188. if (!tableData || !tableData.meta || !tableData.data) {
  189. return cells;
  190. }
  191. tableData.data.forEach((row, i: number) => {
  192. // Another check to appease tsc
  193. if (!tableData.meta) {
  194. return;
  195. }
  196. cells = cells.concat(this.renderRow(row, i, columnOrder, tableData.meta));
  197. });
  198. return cells;
  199. }
  200. render() {
  201. const {isLoading, organization, tableData} = this.props;
  202. const hasResults =
  203. tableData && tableData.data && tableData.meta && tableData.data.length > 0;
  204. const replayIds = tableData?.data?.map(row => row.replayId);
  205. // Custom set the height so we don't have layout shift when results are loaded.
  206. const loader = <LoadingIndicator style={{margin: '70px auto'}} />;
  207. return (
  208. <ReplayIdCountProvider organization={organization} replayIds={replayIds}>
  209. <VisuallyCompleteWithData
  210. id="TransactionsTable"
  211. hasData={hasResults}
  212. isLoading={isLoading}
  213. >
  214. <PanelTable
  215. data-test-id="transactions-table"
  216. isEmpty={!hasResults}
  217. emptyMessage={t('No transactions found')}
  218. headers={this.renderHeader()}
  219. isLoading={isLoading}
  220. disablePadding
  221. loader={loader}
  222. >
  223. {this.renderResults()}
  224. </PanelTable>
  225. </VisuallyCompleteWithData>
  226. </ReplayIdCountProvider>
  227. );
  228. }
  229. }
  230. function getProfileAnalyticsHandler(organization: Organization, referrer?: string) {
  231. return () => {
  232. let source;
  233. if (referrer === 'performance.transactions_summary') {
  234. source = 'performance.transactions_summary.overview';
  235. } else {
  236. source = 'discover.transactions_table';
  237. }
  238. trackAnalytics('profiling_views.go_to_flamegraph', {
  239. organization,
  240. source,
  241. });
  242. };
  243. }
  244. const HeadCellContainer = styled('div')`
  245. padding: ${space(2)};
  246. `;
  247. const BodyCellContainer = styled('div')`
  248. padding: ${space(1)} ${space(2)};
  249. ${p => p.theme.overflowEllipsis};
  250. `;
  251. const StyledIconQuestion = styled(QuestionTooltip)`
  252. position: relative;
  253. top: 1px;
  254. left: 4px;
  255. `;
  256. export default TransactionsTable;