transactionsTable.tsx 7.0 KB

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