spansTable.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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 {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 {fieldAlignment} 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. TableRow,
  24. TableStatus,
  25. useTableStyles,
  26. } from 'sentry/views/explore/components/table';
  27. import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
  28. import {useAnalytics} from 'sentry/views/explore/hooks/useAnalytics';
  29. import {useDataset} from 'sentry/views/explore/hooks/useDataset';
  30. import {useSampleFields} from 'sentry/views/explore/hooks/useSampleFields';
  31. import {useSorts} from 'sentry/views/explore/hooks/useSorts';
  32. import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery';
  33. import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
  34. import {useSpansQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
  35. import {FieldRenderer} from './fieldRenderer';
  36. interface SpansTableProps {
  37. setError: Dispatch<SetStateAction<string>>;
  38. }
  39. export function SpansTable({setError}: SpansTableProps) {
  40. const {selection} = usePageFilters();
  41. const [dataset] = useDataset({allowRPC: true});
  42. const [fields] = useSampleFields();
  43. const [sorts, setSorts] = useSorts({fields});
  44. const [query] = useUserQuery();
  45. const [visualizes] = useVisualizes();
  46. const organization = useOrganization();
  47. const eventView = useMemo(() => {
  48. const queryFields = [
  49. ...fields,
  50. 'project',
  51. 'trace',
  52. 'transaction.span_id',
  53. 'id',
  54. 'timestamp',
  55. ];
  56. const search = new MutableSearch(query);
  57. // Filtering out all spans with op like 'ui.interaction*' which aren't
  58. // embedded under transactions. The trace view does not support rendering
  59. // such spans yet.
  60. search.addFilterValues('!transaction.span_id', ['00']);
  61. const discoverQuery: NewQuery = {
  62. id: undefined,
  63. name: 'Explore - Span Samples',
  64. fields: queryFields,
  65. orderby: sorts.map(sort => `${sort.kind === 'desc' ? '-' : ''}${sort.field}`),
  66. query: search.formatString(),
  67. version: 2,
  68. dataset,
  69. };
  70. return EventView.fromNewQueryWithPageFilters(discoverQuery, selection);
  71. }, [dataset, fields, sorts, query, selection]);
  72. const columns = useMemo(() => eventView.getColumns(), [eventView]);
  73. const result = useSpansQuery({
  74. eventView,
  75. initialData: [],
  76. referrer: 'api.explore.spans-samples-table',
  77. allowAggregateConditions: false,
  78. });
  79. useEffect(() => {
  80. setError(result.error?.message ?? '');
  81. }, [setError, result.error?.message]);
  82. useAnalytics({
  83. resultLength: result.data?.length,
  84. resultMode: 'span samples',
  85. resultStatus: result.status,
  86. visualizes,
  87. organization,
  88. columns: fields,
  89. userQuery: query,
  90. });
  91. const visibleFields = useMemo(
  92. () => (fields.includes('id') ? fields : ['id', ...fields]),
  93. [fields]
  94. );
  95. const {tableStyles} = useTableStyles({
  96. items: visibleFields.map(field => {
  97. return {
  98. label: field,
  99. value: field,
  100. };
  101. }),
  102. });
  103. const meta = result.meta ?? {};
  104. const numberTags = useSpanTags('number');
  105. const stringTags = useSpanTags('string');
  106. return (
  107. <Fragment>
  108. <Table style={tableStyles}>
  109. <TableHead>
  110. <TableRow>
  111. {visibleFields.map((field, i) => {
  112. // Hide column names before alignment is determined
  113. if (result.isPending) {
  114. return <TableHeadCell key={i} isFirst={i === 0} />;
  115. }
  116. const fieldType = meta.fields?.[field];
  117. const align = fieldAlignment(field, fieldType);
  118. const tag = stringTags[field] ?? numberTags[field] ?? null;
  119. const direction = sorts.find(s => s.field === field)?.kind;
  120. function updateSort() {
  121. const kind = direction === 'desc' ? 'asc' : 'desc';
  122. setSorts([{field, kind}]);
  123. }
  124. return (
  125. <StyledTableHeadCell
  126. align={align}
  127. key={i}
  128. isFirst={i === 0}
  129. onClick={updateSort}
  130. >
  131. <span>{tag?.name ?? field}</span>
  132. {defined(direction) && (
  133. <IconArrow
  134. size="xs"
  135. direction={
  136. direction === 'desc'
  137. ? 'down'
  138. : direction === 'asc'
  139. ? 'up'
  140. : undefined
  141. }
  142. />
  143. )}
  144. </StyledTableHeadCell>
  145. );
  146. })}
  147. </TableRow>
  148. </TableHead>
  149. <TableBody>
  150. {result.isPending ? (
  151. <TableStatus>
  152. <LoadingIndicator />
  153. </TableStatus>
  154. ) : result.isError ? (
  155. <TableStatus>
  156. <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
  157. </TableStatus>
  158. ) : result.isFetched && result.data?.length ? (
  159. result.data?.slice(0, 50)?.map((row, i) => (
  160. <TableRow key={i}>
  161. {visibleFields.map((field, j) => {
  162. return (
  163. <TableBodyCell key={j}>
  164. <FieldRenderer
  165. column={columns[j]}
  166. data={row}
  167. unit={meta?.units?.[field]}
  168. meta={meta}
  169. />
  170. </TableBodyCell>
  171. );
  172. })}
  173. </TableRow>
  174. ))
  175. ) : (
  176. <TableStatus>
  177. <EmptyStateWarning>
  178. <p>{t('No spans found')}</p>
  179. </EmptyStateWarning>
  180. </TableStatus>
  181. )}
  182. </TableBody>
  183. </Table>
  184. <Pagination pageLinks={result.pageLinks} />
  185. </Fragment>
  186. );
  187. }
  188. const StyledTableHeadCell = styled(TableHeadCell)`
  189. cursor: pointer;
  190. `;