spansTable.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import type {Dispatch, SetStateAction} from 'react';
  2. import {Fragment, useEffect, useMemo, useRef} from 'react';
  3. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  4. import {GridResizer} from 'sentry/components/gridEditable/styles';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import Pagination from 'sentry/components/pagination';
  7. import {IconArrow} from 'sentry/icons/iconArrow';
  8. import {IconWarning} from 'sentry/icons/iconWarning';
  9. import {t} from 'sentry/locale';
  10. import type {Confidence, NewQuery} from 'sentry/types/organization';
  11. import {defined} from 'sentry/utils';
  12. import EventView from 'sentry/utils/discover/eventView';
  13. import {fieldAlignment, prettifyTagKey} from 'sentry/utils/discover/fields';
  14. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import usePageFilters from 'sentry/utils/usePageFilters';
  17. import {
  18. Table,
  19. TableBody,
  20. TableBodyCell,
  21. TableHead,
  22. TableHeadCell,
  23. TableHeadCellContent,
  24. TableRow,
  25. TableStatus,
  26. useTableStyles,
  27. } from 'sentry/views/explore/components/table';
  28. import {
  29. useExploreDataset,
  30. useExploreFields,
  31. useExploreQuery,
  32. useExploreSortBys,
  33. useExploreTitle,
  34. useExploreVisualizes,
  35. useSetExploreSortBys,
  36. } from 'sentry/views/explore/contexts/pageParamsContext';
  37. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  38. import {useAnalytics} from 'sentry/views/explore/hooks/useAnalytics';
  39. import {useSpansQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
  40. import {FieldRenderer} from './fieldRenderer';
  41. interface SpansTableProps {
  42. confidence: Confidence;
  43. setError: Dispatch<SetStateAction<string>>;
  44. }
  45. export function SpansTable({confidence, setError}: SpansTableProps) {
  46. const {selection} = usePageFilters();
  47. const dataset = useExploreDataset();
  48. const title = useExploreTitle();
  49. const fields = useExploreFields();
  50. const sortBys = useExploreSortBys();
  51. const setSortBys = useSetExploreSortBys();
  52. const query = useExploreQuery();
  53. const visualizes = useExploreVisualizes();
  54. const organization = useOrganization();
  55. const visibleFields = useMemo(
  56. () => (fields.includes('id') ? fields : ['id', ...fields]),
  57. [fields]
  58. );
  59. const eventView = useMemo(() => {
  60. const queryFields = [
  61. ...visibleFields,
  62. 'project',
  63. 'trace',
  64. 'transaction.span_id',
  65. 'id',
  66. 'timestamp',
  67. ];
  68. const search = new MutableSearch(query);
  69. // Filtering out all spans with op like 'ui.interaction*' which aren't
  70. // embedded under transactions. The trace view does not support rendering
  71. // such spans yet.
  72. search.addFilterValues('!transaction.span_id', ['00']);
  73. const discoverQuery: NewQuery = {
  74. id: undefined,
  75. name: 'Explore - Span Samples',
  76. fields: queryFields,
  77. orderby: sortBys.map(sort => `${sort.kind === 'desc' ? '-' : ''}${sort.field}`),
  78. query: search.formatString(),
  79. version: 2,
  80. dataset,
  81. };
  82. return EventView.fromNewQueryWithPageFilters(discoverQuery, selection);
  83. }, [dataset, sortBys, query, selection, visibleFields]);
  84. const columnsFromEventView = useMemo(() => eventView.getColumns(), [eventView]);
  85. const result = useSpansQuery({
  86. eventView,
  87. initialData: [],
  88. referrer: 'api.explore.spans-samples-table',
  89. allowAggregateConditions: false,
  90. });
  91. useEffect(() => {
  92. setError(result.error?.message ?? '');
  93. }, [setError, result.error?.message]);
  94. useAnalytics({
  95. dataset,
  96. resultLength: result.data?.length,
  97. resultMode: 'span samples',
  98. resultStatus: result.status,
  99. visualizes,
  100. organization,
  101. columns: fields,
  102. userQuery: query,
  103. confidence,
  104. title,
  105. });
  106. const tableRef = useRef<HTMLTableElement>(null);
  107. const {initialTableStyles, onResizeMouseDown} = useTableStyles(visibleFields, tableRef);
  108. const meta = result.meta ?? {};
  109. const numberTags = useSpanTags('number');
  110. const stringTags = useSpanTags('string');
  111. return (
  112. <Fragment>
  113. <Table ref={tableRef} styles={initialTableStyles}>
  114. <TableHead>
  115. <TableRow>
  116. {visibleFields.map((field, i) => {
  117. // Hide column names before alignment is determined
  118. if (result.isPending) {
  119. return <TableHeadCell key={i} isFirst={i === 0} />;
  120. }
  121. const fieldType = meta.fields?.[field];
  122. const align = fieldAlignment(field, fieldType);
  123. const tag = stringTags[field] ?? numberTags[field] ?? null;
  124. const direction = sortBys.find(s => s.field === field)?.kind;
  125. function updateSort() {
  126. const kind = direction === 'desc' ? 'asc' : 'desc';
  127. setSortBys([{field, kind}]);
  128. }
  129. return (
  130. <TableHeadCell align={align} key={i} isFirst={i === 0}>
  131. <TableHeadCellContent onClick={updateSort}>
  132. <span>{tag?.name ?? prettifyTagKey(field)}</span>
  133. {defined(direction) && (
  134. <IconArrow
  135. size="xs"
  136. direction={
  137. direction === 'desc'
  138. ? 'down'
  139. : direction === 'asc'
  140. ? 'up'
  141. : undefined
  142. }
  143. />
  144. )}
  145. </TableHeadCellContent>
  146. {i !== visibleFields.length - 1 && (
  147. <GridResizer
  148. dataRows={
  149. !result.isError && !result.isPending && result.data
  150. ? result.data.length
  151. : 0
  152. }
  153. onMouseDown={e => onResizeMouseDown(e, i)}
  154. />
  155. )}
  156. </TableHeadCell>
  157. );
  158. })}
  159. </TableRow>
  160. </TableHead>
  161. <TableBody>
  162. {result.isPending ? (
  163. <TableStatus>
  164. <LoadingIndicator />
  165. </TableStatus>
  166. ) : result.isError ? (
  167. <TableStatus>
  168. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  169. </TableStatus>
  170. ) : result.isFetched && result.data?.length ? (
  171. result.data?.map((row, i) => (
  172. <TableRow key={i}>
  173. {visibleFields.map((field, j) => {
  174. return (
  175. <TableBodyCell key={j}>
  176. <FieldRenderer
  177. column={columnsFromEventView[j]!}
  178. data={row}
  179. unit={meta?.units?.[field]}
  180. meta={meta}
  181. />
  182. </TableBodyCell>
  183. );
  184. })}
  185. </TableRow>
  186. ))
  187. ) : (
  188. <TableStatus>
  189. <EmptyStateWarning>
  190. <p>{t('No spans found')}</p>
  191. </EmptyStateWarning>
  192. </TableStatus>
  193. )}
  194. </TableBody>
  195. </Table>
  196. <Pagination pageLinks={result.pageLinks} />
  197. </Fragment>
  198. );
  199. }