transactionsTable.tsx 8.3 KB

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