aggregatesTable.tsx 7.6 KB

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