aggregatesTable.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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 {CHART_PALETTE} 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 numberTags = useSpanTags('number');
  64. const stringTags = useSpanTags('string');
  65. return (
  66. <Fragment>
  67. <Table ref={tableRef} styles={initialTableStyles}>
  68. <TableHead>
  69. <TableRow>
  70. <TableHeadCell isFirst={false}>
  71. <TableHeadCellContent />
  72. </TableHeadCell>
  73. {fields.map((field, i) => {
  74. // Hide column names before alignment is determined
  75. if (result.isPending) {
  76. return <TableHeadCell key={i} isFirst={i === 0} />;
  77. }
  78. let label = field;
  79. const fieldType = meta.fields?.[field];
  80. const align = fieldAlignment(field, fieldType);
  81. const tag = stringTags[field] ?? numberTags[field] ?? null;
  82. if (tag) {
  83. label = tag.name;
  84. }
  85. const func = parseFunction(field);
  86. if (func) {
  87. label = prettifyParsedFunction(func);
  88. }
  89. const direction = sorts.find(s => s.field === field)?.kind;
  90. function updateSort() {
  91. const kind = direction === 'desc' ? 'asc' : 'desc';
  92. setSorts([{field, kind}]);
  93. }
  94. return (
  95. <TableHeadCell align={align} key={i} isFirst={i === 0}>
  96. <TableHeadCellContent onClick={updateSort}>
  97. <Tooltip showOnlyOnOverflow title={label}>
  98. {label}
  99. </Tooltip>
  100. {defined(direction) && (
  101. <IconArrow
  102. size="xs"
  103. direction={
  104. direction === 'desc'
  105. ? 'down'
  106. : direction === 'asc'
  107. ? 'up'
  108. : undefined
  109. }
  110. />
  111. )}
  112. </TableHeadCellContent>
  113. {i !== fields.length - 1 && (
  114. <GridResizer
  115. dataRows={
  116. !result.isError && !result.isPending && result.data
  117. ? result.data.length
  118. : 0
  119. }
  120. onMouseDown={e => onResizeMouseDown(e, i)}
  121. />
  122. )}
  123. </TableHeadCell>
  124. );
  125. })}
  126. </TableRow>
  127. </TableHead>
  128. <TableBody>
  129. {result.isPending ? (
  130. <TableStatus>
  131. <LoadingIndicator />
  132. </TableStatus>
  133. ) : result.isError ? (
  134. <TableStatus>
  135. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  136. </TableStatus>
  137. ) : result.isFetched && result.data?.length ? (
  138. result.data?.map((row, i) => {
  139. const target = viewSamplesTarget(location, query, groupBys, row, {
  140. projects,
  141. });
  142. return (
  143. <TableRow key={i}>
  144. <TableBodyCell>
  145. {topEvents && i < topEvents && <TopResultsIndicator index={i} />}
  146. <Tooltip title={t('View Samples')} containerDisplayMode="flex">
  147. <StyledLink to={target}>
  148. <IconStack />
  149. </StyledLink>
  150. </Tooltip>
  151. </TableBodyCell>
  152. {fields.map((field, j) => {
  153. return (
  154. <TableBodyCell key={j}>
  155. <FieldRenderer
  156. column={columns[j]!}
  157. data={row}
  158. unit={meta?.units?.[field]}
  159. meta={meta}
  160. />
  161. </TableBodyCell>
  162. );
  163. })}
  164. </TableRow>
  165. );
  166. })
  167. ) : (
  168. <TableStatus>
  169. <EmptyStateWarning>
  170. <p>{t('No spans found')}</p>
  171. </EmptyStateWarning>
  172. </TableStatus>
  173. )}
  174. </TableBody>
  175. </Table>
  176. <Pagination pageLinks={result.pageLinks} />
  177. </Fragment>
  178. );
  179. }
  180. const TopResultsIndicator = styled('div')<{index: number}>`
  181. position: absolute;
  182. left: -1px;
  183. width: 9px;
  184. height: 16px;
  185. border-radius: 0 3px 3px 0;
  186. background-color: ${p => {
  187. return CHART_PALETTE[TOP_EVENTS_LIMIT - 1]![p.index];
  188. }};
  189. `;
  190. const StyledLink = styled(Link)`
  191. display: flex;
  192. `;