aggregatesTable.tsx 8.6 KB

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