aggregatesTable.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {Fragment, useMemo, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  4. import {GridResizer} from 'sentry/components/gridEditable/styles';
  5. import Link from 'sentry/components/links/link';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import Pagination from 'sentry/components/pagination';
  8. import {Tooltip} from 'sentry/components/tooltip';
  9. import {getChartColorPalette} from 'sentry/constants/chartPalette';
  10. import {IconArrow} from 'sentry/icons/iconArrow';
  11. import {IconStack} from 'sentry/icons/iconStack';
  12. import {IconWarning} from 'sentry/icons/iconWarning';
  13. import {t} from 'sentry/locale';
  14. import {defined} from 'sentry/utils';
  15. import {
  16. fieldAlignment,
  17. parseFunction,
  18. prettifyParsedFunction,
  19. } from 'sentry/utils/discover/fields';
  20. import {useLocation} from 'sentry/utils/useLocation';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import {
  23. Table,
  24. TableBody,
  25. TableBodyCell,
  26. TableHead,
  27. TableHeadCell,
  28. TableHeadCellContent,
  29. TableRow,
  30. TableStatus,
  31. useTableStyles,
  32. } from 'sentry/views/explore/components/table';
  33. import {
  34. useExploreGroupBys,
  35. useExploreQuery,
  36. useExploreSortBys,
  37. useSetExploreSortBys,
  38. } from 'sentry/views/explore/contexts/pageParamsContext';
  39. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  40. import type {AggregatesTableResult} from 'sentry/views/explore/hooks/useExploreAggregatesTable';
  41. import {TOP_EVENTS_LIMIT, useTopEvents} from 'sentry/views/explore/hooks/useTopEvents';
  42. import {viewSamplesTarget} from 'sentry/views/explore/utils';
  43. import {FieldRenderer} from './fieldRenderer';
  44. interface AggregatesTableProps {
  45. aggregatesTableResult: AggregatesTableResult;
  46. }
  47. export function AggregatesTable({aggregatesTableResult}: AggregatesTableProps) {
  48. const location = useLocation();
  49. const {projects} = useProjects();
  50. const topEvents = useTopEvents();
  51. const groupBys = useExploreGroupBys();
  52. const {result, eventView, fields} = aggregatesTableResult;
  53. const sorts = useExploreSortBys();
  54. const setSorts = useSetExploreSortBys();
  55. const query = useExploreQuery();
  56. const columns = useMemo(() => eventView.getColumns(), [eventView]);
  57. const tableRef = useRef<HTMLTableElement>(null);
  58. const {initialTableStyles, onResizeMouseDown} = useTableStyles(fields, tableRef, {
  59. minimumColumnWidth: 50,
  60. prefixColumnWidth: 'min-content',
  61. });
  62. const meta = result.meta ?? {};
  63. const {tags: numberTags} = useSpanTags('number');
  64. const {tags: stringTags} = useSpanTags('string');
  65. const numberOfRowsNeedingColor = Math.min(result.data?.length ?? 0, TOP_EVENTS_LIMIT);
  66. const palette = getChartColorPalette(numberOfRowsNeedingColor - 2)!; // -2 because getColorPalette artificially adds 1, I'm not sure why
  67. return (
  68. <Fragment>
  69. <Table ref={tableRef} styles={initialTableStyles}>
  70. <TableHead>
  71. <TableRow>
  72. <TableHeadCell isFirst={false}>
  73. <TableHeadCellContent />
  74. </TableHeadCell>
  75. {fields.map((field, i) => {
  76. // Hide column names before alignment is determined
  77. if (result.isPending) {
  78. return <TableHeadCell key={i} isFirst={i === 0} />;
  79. }
  80. let label = field;
  81. const fieldType = meta.fields?.[field];
  82. const align = fieldAlignment(field, fieldType);
  83. const tag = stringTags[field] ?? numberTags[field] ?? null;
  84. if (tag) {
  85. label = tag.name;
  86. }
  87. const func = parseFunction(field);
  88. if (func) {
  89. label = prettifyParsedFunction(func);
  90. }
  91. const direction = sorts.find(s => s.field === field)?.kind;
  92. function updateSort() {
  93. const kind = direction === 'desc' ? 'asc' : 'desc';
  94. setSorts([{field, kind}]);
  95. }
  96. return (
  97. <TableHeadCell align={align} key={i} isFirst={i === 0}>
  98. <TableHeadCellContent onClick={updateSort}>
  99. <Tooltip showOnlyOnOverflow title={label}>
  100. {label}
  101. </Tooltip>
  102. {defined(direction) && (
  103. <IconArrow
  104. size="xs"
  105. direction={
  106. direction === 'desc'
  107. ? 'down'
  108. : direction === 'asc'
  109. ? 'up'
  110. : undefined
  111. }
  112. />
  113. )}
  114. </TableHeadCellContent>
  115. {i !== fields.length - 1 && (
  116. <GridResizer
  117. dataRows={
  118. !result.isError && !result.isPending && result.data
  119. ? result.data.length
  120. : 0
  121. }
  122. onMouseDown={e => onResizeMouseDown(e, i)}
  123. />
  124. )}
  125. </TableHeadCell>
  126. );
  127. })}
  128. </TableRow>
  129. </TableHead>
  130. <TableBody>
  131. {result.isPending ? (
  132. <TableStatus>
  133. <LoadingIndicator />
  134. </TableStatus>
  135. ) : result.isError ? (
  136. <TableStatus>
  137. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  138. </TableStatus>
  139. ) : result.isFetched && result.data?.length ? (
  140. result.data?.map((row, i) => {
  141. const target = viewSamplesTarget(location, query, groupBys, row, {
  142. projects,
  143. });
  144. return (
  145. <TableRow key={i}>
  146. <TableBodyCell>
  147. {topEvents && i < topEvents && (
  148. <TopResultsIndicator color={palette[i]!} />
  149. )}
  150. <Tooltip title={t('View Samples')} containerDisplayMode="flex">
  151. <StyledLink to={target}>
  152. <IconStack />
  153. </StyledLink>
  154. </Tooltip>
  155. </TableBodyCell>
  156. {fields.map((field, j) => {
  157. return (
  158. <TableBodyCell key={j}>
  159. <FieldRenderer
  160. column={columns[j]!}
  161. data={row}
  162. unit={meta?.units?.[field]}
  163. meta={meta}
  164. />
  165. </TableBodyCell>
  166. );
  167. })}
  168. </TableRow>
  169. );
  170. })
  171. ) : (
  172. <TableStatus>
  173. <EmptyStateWarning>
  174. <p>{t('No spans found')}</p>
  175. </EmptyStateWarning>
  176. </TableStatus>
  177. )}
  178. </TableBody>
  179. </Table>
  180. <Pagination pageLinks={result.pageLinks} />
  181. </Fragment>
  182. );
  183. }
  184. const TopResultsIndicator = styled('div')<{color: string}>`
  185. position: absolute;
  186. left: -1px;
  187. width: 9px;
  188. height: 16px;
  189. border-radius: 0 3px 3px 0;
  190. background-color: ${p => p.color};
  191. `;
  192. const StyledLink = styled(Link)`
  193. display: flex;
  194. `;