aggregatesTable.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import type {Dispatch, SetStateAction} from 'react';
  2. import {Fragment, useEffect, useMemo} from 'react';
  3. import styled from '@emotion/styled';
  4. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import Pagination from 'sentry/components/pagination';
  7. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  8. import {IconArrow} from 'sentry/icons/iconArrow';
  9. import {IconWarning} from 'sentry/icons/iconWarning';
  10. import {t} from 'sentry/locale';
  11. import type {NewQuery} from 'sentry/types/organization';
  12. import {defined} from 'sentry/utils';
  13. import EventView from 'sentry/utils/discover/eventView';
  14. import type {Sort} from 'sentry/utils/discover/fields';
  15. import {
  16. fieldAlignment,
  17. formatParsedFunction,
  18. getAggregateAlias,
  19. parseFunction,
  20. } from 'sentry/utils/discover/fields';
  21. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import usePageFilters from 'sentry/utils/usePageFilters';
  24. import {
  25. Table,
  26. TableBody,
  27. TableBodyCell,
  28. TableHead,
  29. TableHeadCell,
  30. TableRow,
  31. TableStatus,
  32. useTableStyles,
  33. } from 'sentry/views/explore/components/table';
  34. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  35. import {useAnalytics} from 'sentry/views/explore/hooks/useAnalytics';
  36. import {useDataset} from 'sentry/views/explore/hooks/useDataset';
  37. import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys';
  38. import {useSorts} from 'sentry/views/explore/hooks/useSorts';
  39. import {TOP_EVENTS_LIMIT, useTopEvents} from 'sentry/views/explore/hooks/useTopEvents';
  40. import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery';
  41. import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
  42. import {useSpansQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
  43. import {FieldRenderer} from './fieldRenderer';
  44. export function formatSort(sort: Sort): string {
  45. const direction = sort.kind === 'desc' ? '-' : '';
  46. return `${direction}${getAggregateAlias(sort.field)}`;
  47. }
  48. interface AggregatesTableProps {
  49. setError: Dispatch<SetStateAction<string>>;
  50. }
  51. export function AggregatesTable({setError}: AggregatesTableProps) {
  52. const {selection} = usePageFilters();
  53. const topEvents = useTopEvents();
  54. const organization = useOrganization();
  55. const [dataset] = useDataset();
  56. const {groupBys} = useGroupBys();
  57. const [visualizes] = useVisualizes();
  58. const fields = useMemo(() => {
  59. return [...groupBys, ...visualizes.flatMap(visualize => visualize.yAxes)].filter(
  60. Boolean
  61. );
  62. }, [groupBys, visualizes]);
  63. const [sorts, setSorts] = useSorts({fields});
  64. const [query] = useUserQuery();
  65. const eventView = useMemo(() => {
  66. const search = new MutableSearch(query);
  67. // Filtering out all spans with op like 'ui.interaction*' which aren't
  68. // embedded under transactions. The trace view does not support rendering
  69. // such spans yet.
  70. search.addFilterValues('!transaction.span_id', ['00']);
  71. const discoverQuery: NewQuery = {
  72. id: undefined,
  73. name: 'Explore - Span Aggregates',
  74. fields,
  75. orderby: sorts.map(formatSort),
  76. query: search.formatString(),
  77. version: 2,
  78. dataset,
  79. };
  80. return EventView.fromNewQueryWithPageFilters(discoverQuery, selection);
  81. }, [dataset, fields, sorts, query, selection]);
  82. const columns = useMemo(() => eventView.getColumns(), [eventView]);
  83. const result = useSpansQuery({
  84. eventView,
  85. initialData: [],
  86. referrer: 'api.explore.spans-aggregates-table',
  87. });
  88. useEffect(() => {
  89. setError(result.error?.message ?? '');
  90. }, [setError, result.error?.message]);
  91. useAnalytics({
  92. resultLength: result.data?.length,
  93. resultMode: 'aggregates',
  94. resultStatus: result.status,
  95. visualizes,
  96. organization,
  97. columns: groupBys,
  98. userQuery: query,
  99. });
  100. const {tableStyles} = useTableStyles({
  101. items: fields.map(field => {
  102. return {
  103. label: field,
  104. value: field,
  105. };
  106. }),
  107. });
  108. const meta = result.meta ?? {};
  109. const numberTags = useSpanTags('number');
  110. const stringTags = useSpanTags('string');
  111. return (
  112. <Fragment>
  113. <Table style={tableStyles}>
  114. <TableHead>
  115. <TableRow>
  116. {fields.map((field, i) => {
  117. // Hide column names before alignment is determined
  118. if (result.isPending) {
  119. return <TableHeadCell key={i} isFirst={i === 0} />;
  120. }
  121. let label = field;
  122. const fieldType = meta.fields?.[field];
  123. const align = fieldAlignment(field, fieldType);
  124. const tag = stringTags[field] ?? numberTags[field] ?? null;
  125. if (tag) {
  126. label = tag.name;
  127. }
  128. const func = parseFunction(field);
  129. if (func) {
  130. label = formatParsedFunction(func);
  131. }
  132. const direction = sorts.find(s => s.field === field)?.kind;
  133. function updateSort() {
  134. const kind = direction === 'desc' ? 'asc' : 'desc';
  135. setSorts([{field, kind}]);
  136. }
  137. return (
  138. <StyledTableHeadCell
  139. align={align}
  140. key={i}
  141. isFirst={i === 0}
  142. onClick={updateSort}
  143. >
  144. <span>{label}</span>
  145. {defined(direction) && (
  146. <IconArrow
  147. size="xs"
  148. direction={
  149. direction === 'desc'
  150. ? 'down'
  151. : direction === 'asc'
  152. ? 'up'
  153. : undefined
  154. }
  155. />
  156. )}
  157. </StyledTableHeadCell>
  158. );
  159. })}
  160. </TableRow>
  161. </TableHead>
  162. <TableBody>
  163. {result.isPending ? (
  164. <TableStatus>
  165. <LoadingIndicator />
  166. </TableStatus>
  167. ) : result.isError ? (
  168. <TableStatus>
  169. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  170. </TableStatus>
  171. ) : result.isFetched && result.data?.length ? (
  172. result.data?.map((row, i) => (
  173. <TableRow key={i}>
  174. {fields.map((field, j) => {
  175. return (
  176. <TableBodyCell key={j}>
  177. {topEvents && i < topEvents && j === 0 && (
  178. <TopResultsIndicator index={i} />
  179. )}
  180. <FieldRenderer
  181. column={columns[j]}
  182. data={row}
  183. unit={meta?.units?.[field]}
  184. meta={meta}
  185. />
  186. </TableBodyCell>
  187. );
  188. })}
  189. </TableRow>
  190. ))
  191. ) : (
  192. <TableStatus>
  193. <EmptyStateWarning>
  194. <p>{t('No spans found')}</p>
  195. </EmptyStateWarning>
  196. </TableStatus>
  197. )}
  198. </TableBody>
  199. </Table>
  200. <Pagination pageLinks={result.pageLinks} />
  201. </Fragment>
  202. );
  203. }
  204. const TopResultsIndicator = styled('div')<{index: number}>`
  205. position: absolute;
  206. left: -1px;
  207. margin-top: 4.5px;
  208. width: 9px;
  209. height: 15px;
  210. border-radius: 0 3px 3px 0;
  211. background-color: ${p => {
  212. return CHART_PALETTE[TOP_EVENTS_LIMIT - 1][p.index];
  213. }};
  214. `;
  215. const StyledTableHeadCell = styled(TableHeadCell)`
  216. cursor: pointer;
  217. `;