transactionsTable.tsx 6.9 KB

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