aggregatesTable.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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. getAggregateAlias,
  18. parseFunction,
  19. prettifyParsedFunction,
  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({allowRPC: true});
  56. const {groupBys} = useGroupBys();
  57. const [visualizes] = useVisualizes();
  58. const fields = useMemo(() => {
  59. const allFields: string[] = [];
  60. for (const visualize of visualizes) {
  61. for (const yAxis of visualize.yAxes) {
  62. if (allFields.includes(yAxis)) {
  63. continue;
  64. }
  65. allFields.push(yAxis);
  66. }
  67. }
  68. for (const groupBy of groupBys) {
  69. if (allFields.includes(groupBy)) {
  70. continue;
  71. }
  72. allFields.push(groupBy);
  73. }
  74. return allFields.filter(Boolean);
  75. }, [groupBys, visualizes]);
  76. const [sorts, setSorts] = useSorts({fields});
  77. const [query] = useUserQuery();
  78. const eventView = useMemo(() => {
  79. const search = new MutableSearch(query);
  80. // Filtering out all spans with op like 'ui.interaction*' which aren't
  81. // embedded under transactions. The trace view does not support rendering
  82. // such spans yet.
  83. search.addFilterValues('!transaction.span_id', ['00']);
  84. const discoverQuery: NewQuery = {
  85. id: undefined,
  86. name: 'Explore - Span Aggregates',
  87. fields,
  88. orderby: sorts.map(formatSort),
  89. query: search.formatString(),
  90. version: 2,
  91. dataset,
  92. };
  93. return EventView.fromNewQueryWithPageFilters(discoverQuery, selection);
  94. }, [dataset, fields, sorts, query, selection]);
  95. const columns = useMemo(() => eventView.getColumns(), [eventView]);
  96. const result = useSpansQuery({
  97. eventView,
  98. initialData: [],
  99. referrer: 'api.explore.spans-aggregates-table',
  100. });
  101. useEffect(() => {
  102. setError(result.error?.message ?? '');
  103. }, [setError, result.error?.message]);
  104. useAnalytics({
  105. resultLength: result.data?.length,
  106. resultMode: 'aggregates',
  107. resultStatus: result.status,
  108. visualizes,
  109. organization,
  110. columns: groupBys,
  111. userQuery: query,
  112. });
  113. const {tableStyles} = useTableStyles({
  114. items: fields.map(field => {
  115. return {
  116. label: field,
  117. value: field,
  118. };
  119. }),
  120. });
  121. const meta = result.meta ?? {};
  122. const numberTags = useSpanTags('number');
  123. const stringTags = useSpanTags('string');
  124. return (
  125. <Fragment>
  126. <Table style={tableStyles}>
  127. <TableHead>
  128. <TableRow>
  129. {fields.map((field, i) => {
  130. // Hide column names before alignment is determined
  131. if (result.isPending) {
  132. return <TableHeadCell key={i} isFirst={i === 0} />;
  133. }
  134. let label = field;
  135. const fieldType = meta.fields?.[field];
  136. const align = fieldAlignment(field, fieldType);
  137. const tag = stringTags[field] ?? numberTags[field] ?? null;
  138. if (tag) {
  139. label = tag.name;
  140. }
  141. const func = parseFunction(field);
  142. if (func) {
  143. label = prettifyParsedFunction(func);
  144. }
  145. const direction = sorts.find(s => s.field === field)?.kind;
  146. function updateSort() {
  147. const kind = direction === 'desc' ? 'asc' : 'desc';
  148. setSorts([{field, kind}]);
  149. }
  150. return (
  151. <StyledTableHeadCell
  152. align={align}
  153. key={i}
  154. isFirst={i === 0}
  155. onClick={updateSort}
  156. >
  157. <span>{label}</span>
  158. {defined(direction) && (
  159. <IconArrow
  160. size="xs"
  161. direction={
  162. direction === 'desc'
  163. ? 'down'
  164. : direction === 'asc'
  165. ? 'up'
  166. : undefined
  167. }
  168. />
  169. )}
  170. </StyledTableHeadCell>
  171. );
  172. })}
  173. </TableRow>
  174. </TableHead>
  175. <TableBody>
  176. {result.isPending ? (
  177. <TableStatus>
  178. <LoadingIndicator />
  179. </TableStatus>
  180. ) : result.isError ? (
  181. <TableStatus>
  182. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  183. </TableStatus>
  184. ) : result.isFetched && result.data?.length ? (
  185. result.data?.map((row, i) => (
  186. <TableRow key={i}>
  187. {fields.map((field, j) => {
  188. return (
  189. <TableBodyCell key={j}>
  190. {topEvents && i < topEvents && j === 0 && (
  191. <TopResultsIndicator index={i} />
  192. )}
  193. <FieldRenderer
  194. column={columns[j]}
  195. data={row}
  196. unit={meta?.units?.[field]}
  197. meta={meta}
  198. />
  199. </TableBodyCell>
  200. );
  201. })}
  202. </TableRow>
  203. ))
  204. ) : (
  205. <TableStatus>
  206. <EmptyStateWarning>
  207. <p>{t('No spans found')}</p>
  208. </EmptyStateWarning>
  209. </TableStatus>
  210. )}
  211. </TableBody>
  212. </Table>
  213. <Pagination pageLinks={result.pageLinks} />
  214. </Fragment>
  215. );
  216. }
  217. const TopResultsIndicator = styled('div')<{index: number}>`
  218. position: absolute;
  219. left: -1px;
  220. margin-top: 4.5px;
  221. width: 9px;
  222. height: 15px;
  223. border-radius: 0 3px 3px 0;
  224. background-color: ${p => {
  225. return CHART_PALETTE[TOP_EVENTS_LIMIT - 1][p.index];
  226. }};
  227. `;
  228. const StyledTableHeadCell = styled(TableHeadCell)`
  229. cursor: pointer;
  230. `;