aggregatesTable.tsx 8.2 KB

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