index.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import {memo, MouseEvent, useCallback, useMemo, useRef} from 'react';
  2. import {
  3. AutoSizer,
  4. CellMeasurer,
  5. List as ReactVirtualizedList,
  6. ListRowProps,
  7. } from 'react-virtualized';
  8. import styled from '@emotion/styled';
  9. import Placeholder from 'sentry/components/placeholder';
  10. import {useReplayContext} from 'sentry/components/replays/replayContext';
  11. import {t} from 'sentry/locale';
  12. import type {Crumb} from 'sentry/types/breadcrumbs';
  13. import {getPrevReplayEvent} from 'sentry/utils/replays/getReplayEvent';
  14. import BreadcrumbRow from 'sentry/views/replays/detail/breadcrumbs/breadcrumbRow';
  15. import useScrollToCurrentItem from 'sentry/views/replays/detail/breadcrumbs/useScrollToCurrentItem';
  16. import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
  17. import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer';
  18. import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList';
  19. type Props = {
  20. breadcrumbs: undefined | Crumb[];
  21. startTimestampMs: number;
  22. };
  23. // Ensure this object is created once as it is an input to
  24. // `useVirtualizedList`'s memoization
  25. const cellMeasurer = {
  26. fixedWidth: true,
  27. minHeight: 53,
  28. };
  29. function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) {
  30. const {currentTime, currentHoverTime} = useReplayContext();
  31. const expandPaths = useRef(new Map<number, Set<string>>());
  32. const items = useMemo(
  33. () =>
  34. (breadcrumbs || []).filter(crumb => !['console'].includes(crumb.category || '')),
  35. [breadcrumbs]
  36. );
  37. const listRef = useRef<ReactVirtualizedList>(null);
  38. const itemLookup = useMemo(
  39. () =>
  40. breadcrumbs &&
  41. breadcrumbs
  42. .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
  43. .sort(([a], [b]) => a - b),
  44. [breadcrumbs]
  45. );
  46. const current = useMemo(
  47. () =>
  48. breadcrumbs
  49. ? getPrevReplayEvent({
  50. itemLookup,
  51. items: breadcrumbs,
  52. targetTimestampMs: startTimestampMs + currentTime,
  53. })
  54. : undefined,
  55. [itemLookup, breadcrumbs, currentTime, startTimestampMs]
  56. );
  57. const hovered = useMemo(
  58. () =>
  59. currentHoverTime && breadcrumbs
  60. ? getPrevReplayEvent({
  61. itemLookup,
  62. items: breadcrumbs,
  63. targetTimestampMs: startTimestampMs + currentHoverTime,
  64. })
  65. : undefined,
  66. [itemLookup, breadcrumbs, currentHoverTime, startTimestampMs]
  67. );
  68. const deps = useMemo(() => [items], [items]);
  69. const {cache, updateList} = useVirtualizedList({
  70. cellMeasurer,
  71. ref: listRef,
  72. deps,
  73. });
  74. const handleDimensionChange = useCallback(
  75. (
  76. index: number,
  77. path: string,
  78. expandedState: Record<string, boolean>,
  79. event: MouseEvent<HTMLDivElement>
  80. ) => {
  81. const rowState = expandPaths.current.get(index) || new Set();
  82. if (expandedState[path]) {
  83. rowState.add(path);
  84. } else {
  85. // Collapsed, i.e. its default state, so no need to store state
  86. rowState.delete(path);
  87. }
  88. expandPaths.current.set(index, rowState);
  89. cache.clear(index, 0);
  90. listRef.current?.recomputeGridSize({rowIndex: index});
  91. listRef.current?.forceUpdateGrid();
  92. event.stopPropagation();
  93. },
  94. [cache, expandPaths, listRef]
  95. );
  96. useScrollToCurrentItem({
  97. breadcrumbs,
  98. ref: listRef,
  99. startTimestampMs,
  100. });
  101. const renderRow = ({index, key, style, parent}: ListRowProps) => {
  102. const item = items[index];
  103. return (
  104. <CellMeasurer
  105. cache={cache}
  106. columnIndex={0}
  107. key={key}
  108. parent={parent}
  109. rowIndex={index}
  110. >
  111. <BreadcrumbRow
  112. index={index}
  113. isCurrent={current?.id === item.id}
  114. isHovered={hovered?.id === item.id}
  115. breadcrumb={item}
  116. startTimestampMs={startTimestampMs}
  117. style={style}
  118. expandPaths={Array.from(expandPaths.current.get(index) || [])}
  119. onDimensionChange={handleDimensionChange}
  120. />
  121. </CellMeasurer>
  122. );
  123. };
  124. return (
  125. <FluidHeight>
  126. <BreadcrumbContainer>
  127. {breadcrumbs ? (
  128. <AutoSizer onResize={updateList}>
  129. {({height, width}) => (
  130. <ReactVirtualizedList
  131. deferredMeasurementCache={cache}
  132. height={height}
  133. noRowsRenderer={() => (
  134. <NoRowRenderer unfilteredItems={breadcrumbs} clearSearchTerm={() => {}}>
  135. {t('No breadcrumbs recorded')}
  136. </NoRowRenderer>
  137. )}
  138. overscanRowCount={5}
  139. ref={listRef}
  140. rowCount={items.length}
  141. rowHeight={cache.rowHeight}
  142. rowRenderer={renderRow}
  143. width={width}
  144. />
  145. )}
  146. </AutoSizer>
  147. ) : (
  148. <Placeholder height="100%" />
  149. )}
  150. </BreadcrumbContainer>
  151. </FluidHeight>
  152. );
  153. }
  154. const BreadcrumbContainer = styled('div')`
  155. position: relative;
  156. height: 100%;
  157. overflow: hidden;
  158. border: 1px solid ${p => p.theme.border};
  159. border-radius: ${p => p.theme.borderRadius};
  160. `;
  161. export default memo(Breadcrumbs);