import {memo, MouseEvent, useCallback, useMemo, useRef} from 'react'; import { AutoSizer, CellMeasurer, List as ReactVirtualizedList, ListRowProps, } from 'react-virtualized'; import styled from '@emotion/styled'; import Placeholder from 'sentry/components/placeholder'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import {t} from 'sentry/locale'; import type {Crumb} from 'sentry/types/breadcrumbs'; import {getPrevReplayEvent} from 'sentry/utils/replays/getReplayEvent'; import BreadcrumbRow from 'sentry/views/replays/detail/breadcrumbs/breadcrumbRow'; import useScrollToCurrentItem from 'sentry/views/replays/detail/breadcrumbs/useScrollToCurrentItem'; import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight'; import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer'; import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList'; type Props = { breadcrumbs: undefined | Crumb[]; startTimestampMs: number; }; // Ensure this object is created once as it is an input to // `useVirtualizedList`'s memoization const cellMeasurer = { fixedWidth: true, minHeight: 53, }; function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) { const {currentTime, currentHoverTime} = useReplayContext(); const expandPaths = useRef(new Map>()); const items = useMemo( () => (breadcrumbs || []).filter(crumb => !['console'].includes(crumb.category || '')), [breadcrumbs] ); const listRef = useRef(null); const itemLookup = useMemo( () => breadcrumbs && breadcrumbs .map(({timestamp}, i) => [+new Date(timestamp || ''), i]) .sort(([a], [b]) => a - b), [breadcrumbs] ); const current = useMemo( () => breadcrumbs ? getPrevReplayEvent({ itemLookup, items: breadcrumbs, targetTimestampMs: startTimestampMs + currentTime, }) : undefined, [itemLookup, breadcrumbs, currentTime, startTimestampMs] ); const hovered = useMemo( () => currentHoverTime && breadcrumbs ? getPrevReplayEvent({ itemLookup, items: breadcrumbs, targetTimestampMs: startTimestampMs + currentHoverTime, }) : undefined, [itemLookup, breadcrumbs, currentHoverTime, startTimestampMs] ); const deps = useMemo(() => [items], [items]); const {cache, updateList} = useVirtualizedList({ cellMeasurer, ref: listRef, deps, }); const handleDimensionChange = useCallback( ( index: number, path: string, expandedState: Record, event: MouseEvent ) => { const rowState = expandPaths.current.get(index) || new Set(); if (expandedState[path]) { rowState.add(path); } else { // Collapsed, i.e. its default state, so no need to store state rowState.delete(path); } expandPaths.current.set(index, rowState); cache.clear(index, 0); listRef.current?.recomputeGridSize({rowIndex: index}); listRef.current?.forceUpdateGrid(); event.stopPropagation(); }, [cache, expandPaths, listRef] ); useScrollToCurrentItem({ breadcrumbs, ref: listRef, startTimestampMs, }); const renderRow = ({index, key, style, parent}: ListRowProps) => { const item = items[index]; return ( ); }; return ( {breadcrumbs ? ( {({height, width}) => ( ( {}}> {t('No breadcrumbs recorded')} )} overscanRowCount={5} ref={listRef} rowCount={items.length} rowHeight={cache.rowHeight} rowRenderer={renderRow} width={width} /> )} ) : ( )} ); } const BreadcrumbContainer = styled('div')` position: relative; height: 100%; overflow: hidden; border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; `; export default memo(Breadcrumbs);