import {Fragment, memo, useCallback} from 'react'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import colorFn from 'color'; import {Button, LinkButton} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import type {Series} from 'sentry/components/metrics/chart/types'; import TextOverflow from 'sentry/components/textOverflow'; import {Tooltip} from 'sentry/components/tooltip'; import {IconArrow, IconFilter, IconLightning, IconReleases} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import {getUtcDateString} from 'sentry/utils/dates'; import {DEFAULT_SORT_STATE} from 'sentry/utils/metrics/constants'; import {formatMetricUsingUnit} from 'sentry/utils/metrics/formatters'; import { type FocusedMetricsSeries, MetricSeriesFilterUpdateType, type SortState, } from 'sentry/utils/metrics/types'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils'; export const SummaryTable = memo(function SummaryTable({ series, onRowClick, onColorDotClick, onSortChange, sort = DEFAULT_SORT_STATE as SortState, onRowHover, onRowFilter, }: { onRowClick: (series: FocusedMetricsSeries) => void; onSortChange: (sortState: SortState) => void; series: Series[]; onColorDotClick?: (series: FocusedMetricsSeries) => void; onRowFilter?: ( index: number, series: FocusedMetricsSeries, updateType: MetricSeriesFilterUpdateType ) => void; onRowHover?: (seriesName: string) => void; sort?: SortState; }) { const {selection} = usePageFilters(); const organization = useOrganization(); const canFilter = series.length > 1 && !!onRowFilter; const hasActions = series.some(s => s.release || s.transaction) || canFilter; const hasMultipleSeries = series.length > 1; const changeSort = useCallback( (name: SortState['name']) => { trackAnalytics('ddm.widget.sort', { organization, by: name ?? '(none)', order: sort.order, }); Sentry.metrics.increment('ddm.widget.sort', 1, { tags: { by: name ?? '(none)', order: sort.order, }, }); if (sort.name === name) { if (sort.order === 'desc') { onSortChange(DEFAULT_SORT_STATE as SortState); } else if (sort.order === 'asc') { onSortChange({ name, order: 'desc', }); } else { onSortChange({ name, order: 'asc', }); } } else { onSortChange({ name, order: 'asc', }); } }, [sort, onSortChange, organization] ); const handleRowFilter = useCallback( ( index: number | undefined, row: FocusedMetricsSeries, updateType: MetricSeriesFilterUpdateType ) => { if (index === undefined) { return; } trackAnalytics('ddm.widget.add_row_filter', { organization, }); onRowFilter?.(index, row, updateType); }, [onRowFilter, organization] ); const releaseTo = (release: string) => { return { pathname: `/organizations/${organization.slug}/releases/${encodeURIComponent( release )}/`, query: { pageStart: selection.datetime.start, pageEnd: selection.datetime.end, pageStatsPeriod: selection.datetime.period, project: selection.projects, environment: selection.environments, }, }; }; const transactionTo = (transaction: string) => transactionSummaryRouteWithQuery({ orgSlug: organization.slug, transaction, projectID: selection.projects.map(p => String(p)), query: { query: '', environment: selection.environments, start: selection.datetime.start ? getUtcDateString(selection.datetime.start) : undefined, end: selection.datetime.end ? getUtcDateString(selection.datetime.end) : undefined, statsPeriod: selection.datetime.period, }, }); const rows = series .map(s => { return { ...s, ...getValues(s), }; }) // Filter series with no data .filter(s => s.min !== Infinity) .sort((a, b) => { const {name, order} = sort; if (!name) { return 0; } if (name === 'name') { return order === 'asc' ? a.seriesName.localeCompare(b.seriesName) : b.seriesName.localeCompare(a.seriesName); } const aValue = a[name] ?? 0; const bValue = b[name] ?? 0; return order === 'asc' ? aValue - bValue : bValue - aValue; }); return ( {t('Name')} {t('Avg')} {t('Min')} {t('Max')} {t('Sum')} {t('Value')} {hasActions && } { if (hasMultipleSeries) { onRowHover?.(''); } }} > {rows.map( ({ seriesName, id, groupBy, color, hidden, unit, transaction, release, avg, min, max, sum, total, isEquationSeries, queryIndex, }) => { return ( { if (hasMultipleSeries) { onRowClick({ id, groupBy, }); } }} onMouseEnter={() => { if (hasMultipleSeries) { onRowHover?.(id); } }} > { event.stopPropagation(); if (hasMultipleSeries) { onColorDotClick?.({ id, groupBy, }); } }} > } delay={500} overlayStyle={{maxWidth: '80vw'}} > {seriesName} {formatMetricUsingUnit(avg, unit)} {formatMetricUsingUnit(min, unit)} {formatMetricUsingUnit(max, unit)} {formatMetricUsingUnit(sum, unit)} {formatMetricUsingUnit(total, unit)} {hasActions && ( {transaction && (
)} {release && (
)} {/* do not show add/exclude filter if there's no groupby or if this is an equation */} {Object.keys(groupBy ?? {}).length > 0 && !isEquationSeries && ( { handleRowFilter( queryIndex, { id, groupBy, }, MetricSeriesFilterUpdateType.ADD ); }, }, { key: 'exclude-from-filter', label: t('Exclude from filter'), size: 'sm', onAction: () => { handleRowFilter( queryIndex, { id, groupBy, }, MetricSeriesFilterUpdateType.EXCLUDE ); }, }, ]} trigger={triggerProps => (