import {CSSProperties, useCallback} from 'react';
import styled from '@emotion/styled';
import beautify from 'js-beautify';
import {CodeSnippet} from 'sentry/components/codeSnippet';
import BreadcrumbIcon from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type/icon';
import {getDetails} from 'sentry/components/replays/breadcrumbs/utils';
import {useReplayContext} from 'sentry/components/replays/replayContext';
import {relativeTimeInMs} from 'sentry/components/replays/utils';
import {SVGIconProps} from 'sentry/icons/svgIcon';
import {space} from 'sentry/styles/space';
import {getPrevReplayEvent} from 'sentry/utils/replays/getReplayEvent';
import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
import type {Extraction} from 'sentry/utils/replays/hooks/useExtractedCrumbHtml';
import TimestampButton from 'sentry/views/replays/detail/timestampButton';
type Props = {
mutation: Extraction;
mutations: Extraction[];
startTimestampMs: number;
style: CSSProperties;
};
function DomMutationRow({mutation, mutations, startTimestampMs, style}: Props) {
const {html, crumb: breadcrumb} = mutation;
const {currentTime, currentHoverTime} = useReplayContext();
const {handleMouseEnter, handleMouseLeave, handleClick} =
useCrumbHandlers(startTimestampMs);
const onClickTimestamp = useCallback(
() => handleClick(breadcrumb),
[handleClick, breadcrumb]
);
const onMouseEnter = useCallback(
() => handleMouseEnter(breadcrumb),
[handleMouseEnter, breadcrumb]
);
const onMouseLeave = useCallback(
() => handleMouseLeave(breadcrumb),
[handleMouseLeave, breadcrumb]
);
const breadcrumbs = mutations.map(({crumb}) => crumb);
const current = getPrevReplayEvent({
items: breadcrumbs,
targetTimestampMs: startTimestampMs + currentTime,
allowEqual: true,
allowExact: true,
});
const hovered = currentHoverTime
? getPrevReplayEvent({
items: breadcrumbs,
targetTimestampMs: startTimestampMs + currentHoverTime,
allowEqual: true,
allowExact: true,
})
: undefined;
const hasOccurred =
currentTime >= relativeTimeInMs(breadcrumb.timestamp || 0, startTimestampMs);
const isCurrent = breadcrumb.id === current?.id;
const isHovered = breadcrumb.id === hovered?.id;
const {title} = getDetails(breadcrumb);
return (
{title}
{breadcrumb.message}
{beautify.html(html, {indent_size: 2})}
);
}
const MutationListItem = styled('div')<{
isCurrent: boolean;
isHovered: boolean;
}>`
display: flex;
gap: ${space(1)};
padding: ${space(1)} ${space(1.5)};
border-bottom: 1px solid
${p =>
p.isCurrent ? p.theme.purple300 : p.isHovered ? p.theme.purple200 : 'transparent'};
&:hover {
background-color: ${p => p.theme.hover};
}
/*
Draw a vertical line behind the breadcrumb icon.
The line connects each row together, but is truncated for the first and last items.
*/
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
/* $padding + $half_icon_width - $space_for_the_line */
left: calc(${space(1.5)} + (24px / 2) - 1px);
width: 1px;
height: 100%;
background: ${p => p.theme.gray200};
}
&:first-of-type::after {
top: ${space(1)};
bottom: 0;
}
&:last-of-type::after {
top: 0;
height: ${space(1)};
}
&:only-of-type::after {
height: 0;
}
`;
const List = styled('div')`
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
`;
const Row = styled('div')`
display: flex;
flex-direction: row;
`;
/**
* Taken `from events/interfaces/.../breadcrumbs/types`
*/
const IconWrapper = styled('div')<
{hasOccurred: boolean} & Required>
>`
display: flex;
align-items: center;
justify-content: center;
width: 24px;
min-width: 24px;
height: 24px;
border-radius: 50%;
color: ${p => p.theme.white};
background: ${p => (p.hasOccurred ? p.theme[p.color] ?? p.color : p.theme.purple200)};
/* Make sure the icon is above the line through the back */
z-index: ${p => p.theme.zIndex.initial};
`;
const Title = styled('span')<{hasOccurred?: boolean}>`
color: ${p => (p.hasOccurred ? p.theme.gray400 : p.theme.gray300)};
font-weight: bold;
line-height: ${p => p.theme.text.lineHeightBody};
text-transform: capitalize;
${p => p.theme.overflowEllipsis};
`;
const Selector = styled('p')`
color: ${p => p.theme.gray300};
font-size: ${p => p.theme.fontSizeSmall};
margin-bottom: 0;
`;
const CodeContainer = styled('div')`
margin-top: ${space(1)};
max-height: 400px;
max-width: 100%;
overflow: auto;
`;
export default DomMutationRow;