import {useCallback, useRef} from 'react'; import styled from '@emotion/styled'; import { Panel as BasePanel, PanelHeader as BasePanelHeader, } from 'sentry/components/panels'; import Placeholder from 'sentry/components/placeholder'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import {relativeTimeInMs} from 'sentry/components/replays/utils'; import {t} from 'sentry/locale'; import space from 'sentry/styles/space'; import {Crumb} from 'sentry/types/breadcrumbs'; import {getPrevBreadcrumb} from 'sentry/utils/replays/getBreadcrumb'; import {useCurrentItemScroller} from 'sentry/utils/replays/hooks/useCurrentItemScroller'; import FluidPanel from 'sentry/views/replays/detail/layout/fluidPanel'; import BreadcrumbItem from './breadcrumbItem'; function CrumbPlaceholder({number}: {number: number}) { return ( {[...Array(number)].map((_, i) => ( ))} ); } type Props = {}; function Breadcrumbs({}: Props) { const { clearAllHighlights, currentHoverTime, currentTime, highlight, removeHighlight, replay, setCurrentHoverTime, setCurrentTime, } = useReplayContext(); const event = replay?.getEvent(); const allCrumbs = replay?.getRawCrumbs(); const crumbListContainerRef = useRef(null); useCurrentItemScroller(crumbListContainerRef); const startTimestamp = event?.startTimestamp || 0; const isLoaded = Boolean(event); const crumbs = allCrumbs?.filter(crumb => !['console'].includes(crumb.category || '')) || []; const currentUserAction = getPrevBreadcrumb({ crumbs, targetTimestampMs: startTimestamp * 1000 + currentTime, allowExact: true, }); const closestUserAction = currentHoverTime !== undefined ? getPrevBreadcrumb({ crumbs, targetTimestampMs: startTimestamp * 1000 + (currentHoverTime ?? 0), allowExact: true, }) : undefined; const handleMouseEnter = useCallback( (item: Crumb) => { if (startTimestamp) { setCurrentHoverTime(relativeTimeInMs(item.timestamp ?? '', startTimestamp)); } if (item.data && 'nodeId' in item.data) { // XXX: Kind of hacky, but mouseLeave does not fire if you move from a // crumb to a tooltip clearAllHighlights(); highlight({nodeId: item.data.nodeId, annotation: item.data.label}); } }, [setCurrentHoverTime, startTimestamp, highlight, clearAllHighlights] ); const handleMouseLeave = useCallback( (item: Crumb) => { setCurrentHoverTime(undefined); if (item.data && 'nodeId' in item.data) { removeHighlight({nodeId: item.data.nodeId}); } }, [setCurrentHoverTime, removeHighlight] ); const handleClick = useCallback( (crumb: Crumb) => { crumb.timestamp !== undefined ? setCurrentTime(relativeTimeInMs(crumb.timestamp, startTimestamp)) : null; }, [setCurrentTime, startTimestamp] ); const content = isLoaded ? ( {crumbs.map(crumb => ( ))} ) : ( ); return ( {t('Breadcrumbs')}} > {content} ); } const BreadcrumbContainer = styled('div')` padding: ${space(0.5)}; `; const Panel = styled(BasePanel)` width: 100%; height: 100%; overflow: hidden; margin-bottom: 0; `; const PanelHeader = styled(BasePanelHeader)` background-color: ${p => p.theme.background}; border-bottom: 1px solid ${p => p.theme.innerBorder}; font-size: ${p => p.theme.fontSizeSmall}; color: ${p => p.theme.gray500}; text-transform: capitalize; padding: ${space(1)} ${space(1.5)} ${space(1)}; font-weight: 600; `; const PlaceholderMargin = styled(Placeholder)` margin-bottom: ${space(1)}; width: auto; border-radius: ${p => p.theme.borderRadius}; `; export default Breadcrumbs;