aggregatesTable.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  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 {IconWarning} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import type {NewQuery} from 'sentry/types/organization';
  10. import EventView from 'sentry/utils/discover/eventView';
  11. import type {Sort} from 'sentry/utils/discover/fields';
  12. import {
  13. fieldAlignment,
  14. formatParsedFunction,
  15. getAggregateAlias,
  16. parseFunction,
  17. } from 'sentry/utils/discover/fields';
  18. import usePageFilters from 'sentry/utils/usePageFilters';
  19. import {
  20. Table,
  21. TableBody,
  22. TableBodyCell,
  23. TableHead,
  24. TableHeadCell,
  25. TableRow,
  26. TableStatus,
  27. useTableStyles,
  28. } from 'sentry/views/explore/components/table';
  29. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  30. import {useDataset} from 'sentry/views/explore/hooks/useDataset';
  31. import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys';
  32. import {useSorts} from 'sentry/views/explore/hooks/useSorts';
  33. import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery';
  34. import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
  35. import {useSpansQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
  36. import {TOP_EVENTS_LIMIT, useTopEvents} from '../hooks/useTopEvents';
  37. import {FieldRenderer} from './fieldRenderer';
  38. export function formatSort(sort: Sort): string {
  39. const direction = sort.kind === 'desc' ? '-' : '';
  40. return `${direction}${getAggregateAlias(sort.field)}`;
  41. }
  42. interface AggregatesTableProps {}
  43. export function AggregatesTable({}: AggregatesTableProps) {
  44. const {selection} = usePageFilters();
  45. const topEvents = useTopEvents();
  46. const [dataset] = useDataset();
  47. const {groupBys} = useGroupBys();
  48. const [visualizes] = useVisualizes();
  49. const fields = useMemo(() => {
  50. return [...groupBys, ...visualizes.flatMap(visualize => visualize.yAxes)].filter(
  51. Boolean
  52. );
  53. }, [groupBys, visualizes]);
  54. const [sorts] = useSorts({fields});
  55. const [query] = useUserQuery();
  56. const eventView = useMemo(() => {
  57. const discoverQuery: NewQuery = {
  58. id: undefined,
  59. name: 'Explore - Span Aggregates',
  60. fields,
  61. orderby: sorts.map(formatSort),
  62. query,
  63. version: 2,
  64. dataset,
  65. };
  66. return EventView.fromNewQueryWithPageFilters(discoverQuery, selection);
  67. }, [dataset, fields, sorts, query, selection]);
  68. const columns = useMemo(() => eventView.getColumns(), [eventView]);
  69. const result = useSpansQuery({
  70. eventView,
  71. initialData: [],
  72. referrer: 'api.explore.spans-aggregates-table',
  73. });
  74. const {tableStyles} = useTableStyles({
  75. items: fields.map(field => {
  76. return {
  77. label: field,
  78. value: field,
  79. };
  80. }),
  81. });
  82. const meta = result.meta ?? {};
  83. const numberTags = useSpanTags('number');
  84. const stringTags = useSpanTags('string');
  85. return (
  86. <Fragment>
  87. <Table style={tableStyles}>
  88. <TableHead>
  89. <TableRow>
  90. {fields.map((field, i) => {
  91. // Hide column names before alignment is determined
  92. if (result.isPending) {
  93. return <TableHeadCell key={i} isFirst={i === 0} />;
  94. }
  95. let label = field;
  96. const fieldType = meta.fields?.[field];
  97. const align = fieldAlignment(field, fieldType);
  98. const tag = stringTags[field] ?? numberTags[field] ?? null;
  99. if (tag) {
  100. label = tag.name;
  101. }
  102. const func = parseFunction(field);
  103. if (func) {
  104. label = formatParsedFunction(func);
  105. }
  106. return (
  107. <TableHeadCell align={align} key={i} isFirst={i === 0}>
  108. <span>{label}</span>
  109. </TableHeadCell>
  110. );
  111. })}
  112. </TableRow>
  113. </TableHead>
  114. <TableBody>
  115. {result.isPending ? (
  116. <TableStatus>
  117. <LoadingIndicator />
  118. </TableStatus>
  119. ) : result.isError ? (
  120. <TableStatus>
  121. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  122. </TableStatus>
  123. ) : result.isFetched && result.data?.length ? (
  124. result.data?.map((row, i) => (
  125. <TableRow key={i}>
  126. {fields.map((field, j) => {
  127. return (
  128. <TableBodyCell key={j}>
  129. {topEvents && i < topEvents && j === 0 && (
  130. <TopResultsIndicator index={i} />
  131. )}
  132. <FieldRenderer
  133. column={columns[j]}
  134. data={row}
  135. unit={meta?.units?.[field]}
  136. meta={meta}
  137. />
  138. </TableBodyCell>
  139. );
  140. })}
  141. </TableRow>
  142. ))
  143. ) : (
  144. <TableStatus>
  145. <EmptyStateWarning>
  146. <p>{t('No spans found')}</p>
  147. </EmptyStateWarning>
  148. </TableStatus>
  149. )}
  150. </TableBody>
  151. </Table>
  152. <Pagination pageLinks={result.pageLinks} />
  153. </Fragment>
  154. );
  155. }
  156. const TopResultsIndicator = styled('div')<{index: number}>`
  157. position: absolute;
  158. left: -1px;
  159. margin-top: 4.5px;
  160. width: 9px;
  161. height: 15px;
  162. border-radius: 0 3px 3px 0;
  163. background-color: ${p => {
  164. return CHART_PALETTE[TOP_EVENTS_LIMIT - 1][p.index];
  165. }};
  166. `;