import {Fragment, useCallback, useMemo, useState} from 'react';
import styled from '@emotion/styled';
import debounce from 'lodash/debounce';
import {Button} from 'sentry/components/button';
import Count from 'sentry/components/count';
import EmptyStateWarning, {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning';
import ExternalLink from 'sentry/components/links/externalLink';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import Pagination from 'sentry/components/pagination';
import PerformanceDuration from 'sentry/components/performanceDuration';
import {DEFAULT_PER_PAGE, SPAN_PROPS_DOCS_URL} from 'sentry/constants';
import {IconChevron} from 'sentry/icons/iconChevron';
import {IconWarning} from 'sentry/icons/iconWarning';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {decodeScalar} from 'sentry/utils/queryString';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import useProjects from 'sentry/utils/useProjects';
import {useDataset} from 'sentry/views/explore/hooks/useDataset';
import {type TraceResult, useTraces} from 'sentry/views/explore/hooks/useTraces';
import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery';
import {
Description,
ProjectBadgeWrapper,
ProjectsRenderer,
SpanTimeRenderer,
TraceBreakdownRenderer,
TraceIdRenderer,
} from 'sentry/views/explore/tables/tracesTable/fieldRenderers';
import {SpanTable} from 'sentry/views/explore/tables/tracesTable/spansTable';
import {
BreakdownPanelItem,
EmptyStateText,
EmptyValueContainer,
StyledPanel,
StyledPanelHeader,
StyledPanelItem,
TracePanelContent,
WrappingText,
} from 'sentry/views/explore/tables/tracesTable/styles';
export function TracesTable() {
const [dataset] = useDataset();
const [query] = useUserQuery();
const location = useLocation();
const cursor = decodeScalar(location.query.cursor);
const {data, isPending, isError, getResponseHeader} = useTraces({
dataset,
query,
limit: DEFAULT_PER_PAGE,
sort: '-timestamp',
cursor,
});
const showErrorState = useMemo(() => {
return !isPending && isError;
}, [isPending, isError]);
const showEmptyState = useMemo(() => {
return !isPending && !showErrorState && (data?.data?.length ?? 0) === 0;
}, [data, isPending, showErrorState]);
return (
{t('Trace ID')}
{t('Trace Root')}
{!query ? t('Total Spans') : t('Matching Spans')}
{t('Timeline')}
{t('Duration')}
{t('Timestamp')}
{isPending && (
)}
{showErrorState && ( // TODO: need an error state
)}
{showEmptyState && (
{t('No trace results found')}
{tct('Try adjusting your filters or refer to [docSearchProps].', {
docSearchProps: (
{t('docs for search properties')}
),
})}
)}
{data?.data?.map((trace, i) => (
))}
);
}
function TraceRow({
defaultExpanded,
trace,
query,
}: {
defaultExpanded;
query: string;
trace: TraceResult;
}) {
const {selection} = usePageFilters();
const {projects} = useProjects();
const [expanded, setExpanded] = useState(defaultExpanded);
const [highlightedSliceName, _setHighlightedSliceName] = useState('');
const location = useLocation();
const organization = useOrganization();
const setHighlightedSliceName = useMemo(
() =>
debounce(sliceName => _setHighlightedSliceName(sliceName), 100, {
leading: true,
}),
[_setHighlightedSliceName]
);
const onClickExpand = useCallback(() => setExpanded(e => !e), [setExpanded]);
const selectedProjects = useMemo(() => {
const selectedProjectIds = new Set(
selection.projects.map(project => project.toString())
);
return new Set(
projects
.filter(project => selectedProjectIds.has(project.id))
.map(project => project.slug)
);
}, [projects, selection.projects]);
const traceProjects = useMemo(() => {
const seenProjects: Set = new Set();
const leadingProjects: string[] = [];
const trailingProjects: string[] = [];
for (let i = 0; i < trace.breakdowns.length; i++) {
const project = trace.breakdowns[i].project;
if (!defined(project) || seenProjects.has(project)) {
continue;
}
seenProjects.add(project);
// Priotize projects that are selected in the page filters
if (selectedProjects.has(project)) {
leadingProjects.push(project);
} else {
trailingProjects.push(project);
}
}
return [...leadingProjects, ...trailingProjects];
}, [selectedProjects, trace]);
return (
}
aria-label={t('Toggle trace details')}
aria-expanded={expanded}
size="zero"
borderless
onClick={() =>
trackAnalytics('trace_explorer.toggle_trace_details', {
organization,
expanded,
})
}
/>
trackAnalytics('trace_explorer.open_trace', {
organization,
})
}
location={location}
/>
0
? traceProjects
: trace.project
? [trace.project]
: []
}
/>
{trace.name ? (
{trace.name}
) : (
{t('Missing Trace Root')}
)}
{query ? (
tct('[numerator][space]of[space][denominator]', {
numerator: ,
denominator: ,
space: ,
})
) : (
)}
setHighlightedSliceName('')}
>
{expanded && (
)}
);
}
const StyledButton = styled(Button)`
margin-right: ${space(0.5)};
`;