import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import Avatar from 'sentry/components/avatar';
import UserBadge from 'sentry/components/idBadge/userBadge';
import Link from 'sentry/components/links/link';
import ContextIcon from 'sentry/components/replays/contextIcon';
import {formatTime} from 'sentry/components/replays/utils';
import StringWalker from 'sentry/components/replays/walker/stringWalker';
import ScoreBar from 'sentry/components/scoreBar';
import TimeSince from 'sentry/components/timeSince';
import {CHART_PALETTE} from 'sentry/constants/chartPalette';
import {IconCalendar, IconDelete, IconFire} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space, ValidSize} from 'sentry/styles/space';
import type {Organization} from 'sentry/types';
import {trackAnalytics} from 'sentry/utils/analytics';
import EventView from 'sentry/utils/discover/eventView';
import {spanOperationRelativeBreakdownRenderer} from 'sentry/utils/discover/fieldRenderers';
import {getShortEventId} from 'sentry/utils/events';
import {useLocation} from 'sentry/utils/useLocation';
import useMedia from 'sentry/utils/useMedia';
import useProjects from 'sentry/utils/useProjects';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
import type {ReplayListRecord} from 'sentry/views/replays/types';
type Props = {
replay: ReplayListRecord | ReplayListRecordWithTx;
};
function getUserBadgeUser(replay: Props['replay']) {
return replay.is_archived
? {
username: '',
email: '',
id: '',
ip_address: '',
name: '',
}
: {
username: replay.user?.display_name || '',
email: replay.user?.email || '',
id: replay.user?.id || '',
ip_address: replay.user?.ip || '',
name: replay.user?.username || '',
};
}
export function ReplayCell({
eventView,
organization,
referrer,
replay,
showUrl,
}: Props & {
eventView: EventView;
organization: Organization;
referrer: string;
showUrl: boolean;
}) {
const {projects} = useProjects();
const project = projects.find(p => p.id === replay.project_id);
const replayDetails = {
pathname: normalizeUrl(`/organizations/${organization.slug}/replays/${replay.id}/`),
query: {
referrer,
...eventView.generateQueryStringObject(),
},
};
const trackNavigationEvent = () =>
trackAnalytics('replay.list-navigate-to-details', {
project_id: project?.id,
platform: project?.platform,
organization,
referrer,
});
if (replay.is_archived) {
return (
-
{t('Deleted Replay')}
{project ? : null}
{getShortEventId(replay.id)}
);
}
const subText = (
{showUrl ? : undefined}
{project ? : null}
{getShortEventId(replay.id)}
);
return (
-
{replay.user.display_name || t('Unknown User')}
)
}
user={getUserBadgeUser(replay)}
// this is the subheading for the avatar, so displayEmail in this case is a misnomer
displayEmail={subText}
/>
);
}
const StyledIconDelete = styled(IconDelete)`
margin: ${space(0.25)};
`;
// Need to be full width for StringWalker to take up full width and truncate properly
const UserBadgeFullWidth = styled(UserBadge)`
width: 100%;
`;
const Cols = styled('div')`
display: flex;
flex-direction: column;
gap: ${space(0.5)};
width: 100%;
`;
const Row = styled('div')<{gap: ValidSize; minWidth?: number}>`
display: flex;
gap: ${p => space(p.gap)};
align-items: center;
${p => (p.minWidth ? `min-width: ${p.minWidth}px;` : '')}
`;
const MainLink = styled(Link)`
font-size: ${p => p.theme.fontSizeLarge};
`;
export function TransactionCell({
organization,
replay,
}: Props & {organization: Organization}) {
const location = useLocation();
if (replay.is_archived) {
return ;
}
const hasTxEvent = 'txEvent' in replay;
const txDuration = hasTxEvent ? replay.txEvent?.['transaction.duration'] : undefined;
return hasTxEvent ? (
{txDuration ? {txDuration}ms
: null}
{spanOperationRelativeBreakdownRenderer(
replay.txEvent,
{organization, location},
{enableOnClick: false}
)}
) : null;
}
export function OSCell({replay}: Props) {
const {name, version} = replay.os ?? {};
const theme = useTheme();
const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.large})`);
if (replay.is_archived) {
return ;
}
return (
-
);
}
export function BrowserCell({replay}: Props) {
const {name, version} = replay.browser ?? {};
const theme = useTheme();
const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.large})`);
if (replay.is_archived) {
return ;
}
return (
-
);
}
export function DurationCell({replay}: Props) {
if (replay.is_archived) {
return ;
}
return (
-
);
}
export function RageClickCountCell({replay}: Props) {
if (replay.is_archived) {
return ;
}
return (
-
{replay.count_rage_clicks ? (
{replay.count_rage_clicks}
) : (
0
)}
);
}
export function DeadClickCountCell({replay}: Props) {
if (replay.is_archived) {
return ;
}
return (
-
{replay.count_dead_clicks ? (
{replay.count_dead_clicks}
) : (
0
)}
);
}
export function ErrorCountCell({replay}: Props) {
if (replay.is_archived) {
return ;
}
return (
-
{replay.count_errors ? (
{replay.count_errors}
) : (
0
)}
);
}
export function ActivityCell({replay}: Props) {
if (replay.is_archived) {
return ;
}
const scoreBarPalette = new Array(10).fill([CHART_PALETTE[0][0]]);
return (
-
);
}
const Item = styled('div')<{isArchived?: boolean}>`
display: flex;
align-items: center;
gap: ${space(1)};
padding: ${space(1.5)};
${p => (p.isArchived ? 'opacity: 0.5;' : '')};
`;
const Count = styled('span')`
font-variant-numeric: tabular-nums;
`;
const ErrorCount = styled(Count)`
display: flex;
align-items: center;
gap: ${space(0.5)};
color: ${p => p.theme.red400};
`;
const Time = styled('span')`
font-variant-numeric: tabular-nums;
`;
const SpanOperationBreakdown = styled('div')`
width: 100%;
display: flex;
flex-direction: column;
gap: ${space(0.5)};
color: ${p => p.theme.gray500};
font-size: ${p => p.theme.fontSizeMedium};
text-align: right;
`;