import {Fragment, useCallback} from 'react'; import styled from '@emotion/styled'; import { Panel as BasePanel, PanelBody as BasePanelBody, 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 {EventTransaction} from 'sentry/types/event'; import {getPrevBreadcrumb} from 'sentry/utils/replays/getBreadcrumb'; import BreadcrumbItem from './breadcrumbItem'; function CrumbPlaceholder({number}: {number: number}) { return ( {[...Array(number)].map((_, i) => ( ))} ); } type Props = { /** * Raw breadcrumbs, `undefined` means it is still loading */ crumbs: Crumb[] | undefined; /** * Root replay event, `undefined` means it is still loading */ event: EventTransaction | undefined; }; function Breadcrumbs({event, crumbs: allCrumbs}: Props) { const { setCurrentTime, setCurrentHoverTime, currentHoverTime, currentTime, highlight, removeHighlight, clearAllHighlights, } = useReplayContext(); 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}); } }, [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] ); return ( {t('Breadcrumbs')} {!isLoaded && } {isLoaded && crumbs.map(crumb => ( ))} ); } // FYI: Since the Replay Player has dynamic height based // on the width of the window, // height: 0; will helps us to reset the height // min-height: 100%; will helps us to grow at the same height of Player const Panel = styled(BasePanel)` width: 100%; display: grid; grid-template-rows: auto 1fr; height: 0; min-height: 100%; @media only screen and (max-width: ${p => p.theme.breakpoints.large}) { height: fit-content; max-height: 400px; margin-top: ${space(2)}; } `; const PanelHeader = styled(BasePanelHeader)` background-color: ${p => p.theme.background}; border-bottom: none; font-size: ${p => p.theme.fontSizeSmall}; color: ${p => p.theme.gray300}; text-transform: capitalize; padding: ${space(1.5)} ${space(2)} ${space(0.5)}; `; const PanelBody = styled(BasePanelBody)` overflow-y: auto; `; const PlaceholderMargin = styled(Placeholder)` margin: ${space(1)} ${space(1.5)}; width: auto; `; export default Breadcrumbs;