aggregatesTable.tsx 7.2 KB

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