import {Fragment} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import type {Location} from 'history'; import type {GridColumnHeader} from 'sentry/components/gridEditable'; import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; import Pagination, {type CursorHandler} from 'sentry/components/pagination'; import {ROW_HEIGHT, ROW_PADDING} from 'sentry/components/performance/waterfall/constants'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization, Project} from 'sentry/types'; import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import type {ColumnType} from 'sentry/utils/discover/fields'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; import {decodeScalar} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import {SpanDurationBar} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/spanDetailsTable'; import {SpanSummaryReferrer} from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/referrers'; import {useSpanSummarySort} from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/useSpanSummarySort'; import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell'; import {SpanIdCell} from 'sentry/views/starfish/components/tableCells/spanIdCell'; import {useIndexedSpans} from 'sentry/views/starfish/queries/useIndexedSpans'; import { type IndexedResponse, SpanIndexedField, type SpanMetricsQueryFilters, } from 'sentry/views/starfish/types'; import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters'; type DataRowKeys = | SpanIndexedField.ID | SpanIndexedField.TIMESTAMP | SpanIndexedField.SPAN_DURATION | SpanIndexedField.TRANSACTION_ID | SpanIndexedField.TRACE | SpanIndexedField.PROJECT; type ColumnKeys = | SpanIndexedField.ID | SpanIndexedField.TIMESTAMP | SpanIndexedField.SPAN_DURATION; type DataRow = Pick & {'transaction.duration': number}; type Column = GridColumnHeader; const COLUMN_ORDER: Column[] = [ { key: SpanIndexedField.ID, name: t('Span ID'), width: COL_WIDTH_UNDEFINED, }, { key: SpanIndexedField.TIMESTAMP, name: t('Timestamp'), width: COL_WIDTH_UNDEFINED, }, { key: SpanIndexedField.SPAN_DURATION, name: t('Span Duration'), width: COL_WIDTH_UNDEFINED, }, ]; const COLUMN_TYPE: Omit< Record, 'spans' | 'transactionDuration' > = { span_id: 'string', timestamp: 'date', 'span.duration': 'duration', }; const LIMIT = 8; type Props = { project: Project | undefined; }; export default function SpanSummaryTable(props: Props) { const {project} = props; const organization = useOrganization(); const {spanSlug} = useParams(); const [spanOp, groupId] = spanSlug.split(':'); const location = useLocation(); const {transaction} = location.query; const spansCursor = decodeScalar(location.query?.[QueryParameterNames.SPANS_CURSOR]); const filters: SpanMetricsQueryFilters = { 'span.group': groupId, 'span.op': spanOp, transaction: transaction as string, }; const sort = useSpanSummarySort(); const { data: rowData, pageLinks, isLoading: isRowDataLoading, } = useIndexedSpans({ fields: [ SpanIndexedField.ID, SpanIndexedField.TIMESTAMP, SpanIndexedField.SPAN_DURATION, SpanIndexedField.TRACE, SpanIndexedField.SEGMENT_ID, ], search: MutableSearch.fromQueryObject(filters), limit: LIMIT, referrer: SpanSummaryReferrer.SPAN_SUMMARY_TABLE, sorts: [sort], cursor: spansCursor, }); const segmentIds = rowData?.map(row => row.segment_id); const { data: transactionDurations, isLoading: isTxnDurationDataLoading, isError: isTxnDurationError, } = useIndexedSpans({ fields: [SpanIndexedField.SEGMENT_ID, SpanIndexedField.SPAN_DURATION], search: MutableSearch.fromQueryObject({ project: project?.slug, segment_id: `[${segmentIds?.join() ?? ''}]`, 'span.is_segment': 1, }), limit: LIMIT, referrer: SpanSummaryReferrer.SPAN_SUMMARY_TABLE, enabled: Boolean(segmentIds), }); // Restructure the transaction durations into a map for faster lookup const transactionDurationMap = {}; transactionDurations?.forEach(datum => { transactionDurationMap[datum.segment_id] = datum['span.duration']; }); const mergedData: DataRow[] = rowData?.map((row: Pick) => { const segmentId = row.segment_id; const newRow = { ...row, 'transaction.duration': transactionDurationMap[segmentId], }; return newRow; }) ?? []; const handleCursor: CursorHandler = (cursor, pathname, query) => { browserHistory.push({ pathname, query: {...query, [QueryParameterNames.SPANS_CURSOR]: cursor}, }); }; return ( renderHeadCell({ column, location, sort, }), renderBodyCell: renderBodyCell( location, organization, spanOp, isTxnDurationDataLoading || isTxnDurationError ), }} location={location} /> ); } function renderBodyCell( location: Location, organization: Organization, spanOp: string = '', isTxnDurationDataLoading: boolean ) { return function (column: Column, dataRow: DataRow): React.ReactNode { const {timestamp, span_id, trace, project} = dataRow; const spanDuration = dataRow[SpanIndexedField.SPAN_DURATION]; const transactionId = dataRow[SpanIndexedField.TRANSACTION_ID]; const transactionDuration = dataRow['transaction.duration']; if (column.key === SpanIndexedField.SPAN_DURATION) { if (isTxnDurationDataLoading) { return ; } return ( ); } if (column.key === SpanIndexedField.ID) { return ( ); } const fieldRenderer = getFieldRenderer(column.key, COLUMN_TYPE); const rendered = fieldRenderer(dataRow, {location, organization}); return rendered; }; } const SpanDurationBarLoading = styled('div')` height: ${ROW_HEIGHT - 2 * ROW_PADDING}px; width: 100%; position: relative; display: flex; top: ${space(0.5)}; background-color: ${p => p.theme.gray100}; `;