123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- import type {Dispatch, SetStateAction} from 'react';
- import {Fragment, useEffect, useMemo} from 'react';
- import styled from '@emotion/styled';
- import EmptyStateWarning from 'sentry/components/emptyStateWarning';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import Pagination from 'sentry/components/pagination';
- import {CHART_PALETTE} from 'sentry/constants/chartPalette';
- import {IconArrow} from 'sentry/icons/iconArrow';
- import {IconWarning} from 'sentry/icons/iconWarning';
- import {t} from 'sentry/locale';
- import type {NewQuery} from 'sentry/types/organization';
- import {defined} from 'sentry/utils';
- import EventView from 'sentry/utils/discover/eventView';
- import type {Sort} from 'sentry/utils/discover/fields';
- import {
- fieldAlignment,
- getAggregateAlias,
- parseFunction,
- prettifyParsedFunction,
- } from 'sentry/utils/discover/fields';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import useOrganization from 'sentry/utils/useOrganization';
- import usePageFilters from 'sentry/utils/usePageFilters';
- import {
- Table,
- TableBody,
- TableBodyCell,
- TableHead,
- TableHeadCell,
- TableRow,
- TableStatus,
- useTableStyles,
- } from 'sentry/views/explore/components/table';
- import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
- import {useAnalytics} from 'sentry/views/explore/hooks/useAnalytics';
- import {useDataset} from 'sentry/views/explore/hooks/useDataset';
- import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys';
- import {useSorts} from 'sentry/views/explore/hooks/useSorts';
- import {TOP_EVENTS_LIMIT, useTopEvents} from 'sentry/views/explore/hooks/useTopEvents';
- import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery';
- import {useVisualizes} from 'sentry/views/explore/hooks/useVisualizes';
- import {useSpansQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
- import {FieldRenderer} from './fieldRenderer';
- export function formatSort(sort: Sort): string {
- const direction = sort.kind === 'desc' ? '-' : '';
- return `${direction}${getAggregateAlias(sort.field)}`;
- }
- interface AggregatesTableProps {
- setError: Dispatch<SetStateAction<string>>;
- }
- export function AggregatesTable({setError}: AggregatesTableProps) {
- const {selection} = usePageFilters();
- const topEvents = useTopEvents();
- const organization = useOrganization();
- const [dataset] = useDataset({allowRPC: true});
- const {groupBys} = useGroupBys();
- const [visualizes] = useVisualizes();
- const fields = useMemo(() => {
- const allFields: string[] = [];
- for (const visualize of visualizes) {
- for (const yAxis of visualize.yAxes) {
- if (allFields.includes(yAxis)) {
- continue;
- }
- allFields.push(yAxis);
- }
- }
- for (const groupBy of groupBys) {
- if (allFields.includes(groupBy)) {
- continue;
- }
- allFields.push(groupBy);
- }
- return allFields.filter(Boolean);
- }, [groupBys, visualizes]);
- const [sorts, setSorts] = useSorts({fields});
- const [query] = useUserQuery();
- const eventView = useMemo(() => {
- const search = new MutableSearch(query);
- // Filtering out all spans with op like 'ui.interaction*' which aren't
- // embedded under transactions. The trace view does not support rendering
- // such spans yet.
- search.addFilterValues('!transaction.span_id', ['00']);
- const discoverQuery: NewQuery = {
- id: undefined,
- name: 'Explore - Span Aggregates',
- fields,
- orderby: sorts.map(formatSort),
- query: search.formatString(),
- version: 2,
- dataset,
- };
- return EventView.fromNewQueryWithPageFilters(discoverQuery, selection);
- }, [dataset, fields, sorts, query, selection]);
- const columns = useMemo(() => eventView.getColumns(), [eventView]);
- const result = useSpansQuery({
- eventView,
- initialData: [],
- referrer: 'api.explore.spans-aggregates-table',
- });
- useEffect(() => {
- setError(result.error?.message ?? '');
- }, [setError, result.error?.message]);
- useAnalytics({
- resultLength: result.data?.length,
- resultMode: 'aggregates',
- resultStatus: result.status,
- visualizes,
- organization,
- columns: groupBys,
- userQuery: query,
- });
- const {tableStyles} = useTableStyles({
- items: fields.map(field => {
- return {
- label: field,
- value: field,
- };
- }),
- });
- const meta = result.meta ?? {};
- const numberTags = useSpanTags('number');
- const stringTags = useSpanTags('string');
- return (
- <Fragment>
- <Table style={tableStyles}>
- <TableHead>
- <TableRow>
- {fields.map((field, i) => {
- // Hide column names before alignment is determined
- if (result.isPending) {
- return <TableHeadCell key={i} isFirst={i === 0} />;
- }
- let label = field;
- const fieldType = meta.fields?.[field];
- const align = fieldAlignment(field, fieldType);
- const tag = stringTags[field] ?? numberTags[field] ?? null;
- if (tag) {
- label = tag.name;
- }
- const func = parseFunction(field);
- if (func) {
- label = prettifyParsedFunction(func);
- }
- const direction = sorts.find(s => s.field === field)?.kind;
- function updateSort() {
- const kind = direction === 'desc' ? 'asc' : 'desc';
- setSorts([{field, kind}]);
- }
- return (
- <StyledTableHeadCell
- align={align}
- key={i}
- isFirst={i === 0}
- onClick={updateSort}
- >
- <span>{label}</span>
- {defined(direction) && (
- <IconArrow
- size="xs"
- direction={
- direction === 'desc'
- ? 'down'
- : direction === 'asc'
- ? 'up'
- : undefined
- }
- />
- )}
- </StyledTableHeadCell>
- );
- })}
- </TableRow>
- </TableHead>
- <TableBody>
- {result.isPending ? (
- <TableStatus>
- <LoadingIndicator />
- </TableStatus>
- ) : result.isError ? (
- <TableStatus>
- <IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
- </TableStatus>
- ) : result.isFetched && result.data?.length ? (
- result.data?.map((row, i) => (
- <TableRow key={i}>
- {fields.map((field, j) => {
- return (
- <TableBodyCell key={j}>
- {topEvents && i < topEvents && j === 0 && (
- <TopResultsIndicator index={i} />
- )}
- <FieldRenderer
- column={columns[j]}
- data={row}
- unit={meta?.units?.[field]}
- meta={meta}
- />
- </TableBodyCell>
- );
- })}
- </TableRow>
- ))
- ) : (
- <TableStatus>
- <EmptyStateWarning>
- <p>{t('No spans found')}</p>
- </EmptyStateWarning>
- </TableStatus>
- )}
- </TableBody>
- </Table>
- <Pagination pageLinks={result.pageLinks} />
- </Fragment>
- );
- }
- const TopResultsIndicator = styled('div')<{index: number}>`
- position: absolute;
- left: -1px;
- margin-top: 4.5px;
- width: 9px;
- height: 15px;
- border-radius: 0 3px 3px 0;
- background-color: ${p => {
- return CHART_PALETTE[TOP_EVENTS_LIMIT - 1][p.index];
- }};
- `;
- const StyledTableHeadCell = styled(TableHeadCell)`
- cursor: pointer;
- `;
|