import {Fragment} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import {Button} from 'sentry/components/button'; import type {DateTimeObject} from 'sentry/components/charts/utils'; import CollapsePanel, {COLLAPSE_COUNT} from 'sentry/components/collapsePanel'; import Link from 'sentry/components/links/link'; import LoadingError from 'sentry/components/loadingError'; import {PanelTable} from 'sentry/components/panels/panelTable'; import {IconStar} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization, Project, SavedQueryVersions} from 'sentry/types'; import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery'; import DiscoverQuery from 'sentry/utils/discover/discoverQuery'; import EventView from 'sentry/utils/discover/eventView'; import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery'; import type {ColorOrAlias} from 'sentry/utils/theme'; import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils'; import {ProjectBadge, ProjectBadgeContainer} from './styles'; import {groupByTrend} from './utils'; type TeamMiseryProps = { isLoading: boolean; location: Location; organization: Organization; periodTableData: TableData | null; projects: Project[]; weekTableData: TableData | null; error?: QueryError | null; period?: string | null; }; function TeamMisery({ organization, location, projects, periodTableData, weekTableData, isLoading, period, error, }: TeamMiseryProps) { const miseryRenderer = periodTableData?.meta && getFieldRenderer('user_misery()', periodTableData.meta, false); // Calculate trend, so we can sort based on it const sortedTableData = (periodTableData?.data ?? []) .map(dataRow => { const weekRow = weekTableData?.data.find( row => row.project === dataRow.project && row.transaction === dataRow.transaction ); const trend = weekRow ? ((dataRow['user_misery()'] as number) - (weekRow['user_misery()'] as number)) * 100 : null; return { ...dataRow, trend, } as TableDataRow & {trend: number}; }) .filter(x => x.trend !== null) .sort((a, b) => Math.abs(b.trend) - Math.abs(a.trend)); const groupedData = groupByTrend(sortedTableData); if (error) { return ; } return ( {({isExpanded, showMoreButton}) => ( {t('Learn More')} } headers={[ {t('Key transaction')} , t('Project'), tct('Last [period]', {period}), t('Last 7 Days'), {t('Change')}, ]} isLoading={isLoading} > {groupedData.map((dataRow, idx) => { const project = projects.find(({slug}) => dataRow.project === slug); const {trend, project: projectId, transaction} = dataRow; const weekRow = weekTableData?.data.find( row => row.project === projectId && row.transaction === transaction ); if (!weekRow || trend === null) { return null; } const periodMisery = miseryRenderer?.(dataRow, {organization, location}); const weekMisery = weekRow && miseryRenderer?.(weekRow, {organization, location}); const trendValue = Math.round(Math.abs(trend)); if (idx >= COLLAPSE_COUNT && !isExpanded) { return null; } return (
{dataRow.transaction}
{project && } {periodMisery} {weekMisery ?? '\u2014'} {trendValue === 0 ? ( {`0\u0025 `} {t('change')} ) : ( = 0 ? 'successText' : 'errorText'}> {`${trendValue}\u0025 `} {trend >= 0 ? t('better') : t('worse')} )}
); })}
{!isLoading && showMoreButton}
)}
); } type Props = { location: Location; organization: Organization; projects: Project[]; teamId: string; end?: string; period?: string | null; start?: string; } & DateTimeObject; function TeamMiseryWrapper({ organization, teamId, projects, location, period, start, end, }: Props) { if (projects.length === 0) { return ( ); } const commonEventView = { id: undefined, query: 'transaction.duration:<15m team_key_transaction:true', projects: [], version: 2 as SavedQueryVersions, orderby: '-tpm', teams: [Number(teamId)], fields: [ 'transaction', 'project', 'tpm()', 'count_unique(user)', 'count_miserable(user)', 'user_misery()', ], }; const periodEventView = EventView.fromSavedQuery({ ...commonEventView, name: 'periodMisery', range: period ?? undefined, start, end, }); const weekEventView = EventView.fromSavedQuery({ ...commonEventView, name: 'weekMisery', range: '7d', }); return ( {({isLoading, tableData: periodTableData, error}) => ( {({isLoading: isWeekLoading, tableData: weekTableData, error: weekError}) => ( )} )} ); } export default TeamMiseryWrapper; const StyledPanelTable = styled(PanelTable)<{isEmpty: boolean}>` grid-template-columns: 1.25fr 0.5fr 112px 112px 0.25fr; font-size: ${p => p.theme.fontSizeMedium}; white-space: nowrap; margin-bottom: 0; border: 0; box-shadow: unset; & > div { padding: ${space(1)} ${space(2)}; } ${p => p.isEmpty && css` & > div:last-child { padding: 48px ${space(2)}; } `} `; const FlexCenter = styled('div')` display: flex; align-items: center; `; const KeyTransactionTitleWrapper = styled('div')` ${p => p.theme.overflowEllipsis}; display: flex; align-items: center; `; const StyledIconStar = styled(IconStar)` display: block; margin-right: ${space(1)}; margin-bottom: ${space(0.5)}; `; const TransactionWrapper = styled('div')` ${p => p.theme.overflowEllipsis}; `; const RightAligned = styled('span')` text-align: right; `; const ScoreWrapper = styled('div')` display: flex; align-items: center; justify-content: flex-end; text-align: right; `; const SubText = styled('div')` color: ${p => p.theme.subText}; `; const TrendText = styled('div')<{color: ColorOrAlias}>` color: ${p => p.theme[p.color]}; `;