index.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import {memo, 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. import useVirtualizedInspector from '../useVirtualizedInspector';
  20. type Props = {
  21. breadcrumbs: undefined | Crumb[];
  22. startTimestampMs: number;
  23. };
  24. // Ensure this object is created once as it is an input to
  25. // `useVirtualizedList`'s memoization
  26. const cellMeasurer = {
  27. fixedWidth: true,
  28. minHeight: 53,
  29. };
  30. function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) {
  31. const {currentTime, currentHoverTime} = useReplayContext();
  32. const listRef = useRef<ReactVirtualizedList>(null);
  33. // Keep a reference of object paths that are expanded (via <ObjectInspector>)
  34. // by log row, so they they can be restored as the Console pane is scrolling.
  35. // Due to virtualization, components can be unmounted as the user scrolls, so
  36. // state needs to be remembered.
  37. //
  38. // Note that this is intentionally not in state because we do not want to
  39. // re-render when items are expanded/collapsed, though it may work in state as well.
  40. const expandPathsRef = useRef(new Map<number, Set<string>>());
  41. const itemLookup = useMemo(
  42. () =>
  43. breadcrumbs &&
  44. breadcrumbs
  45. .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
  46. .sort(([a], [b]) => a - b),
  47. [breadcrumbs]
  48. );
  49. const current = useMemo(
  50. () =>
  51. breadcrumbs
  52. ? getPrevReplayEvent({
  53. itemLookup,
  54. items: breadcrumbs,
  55. targetTimestampMs: startTimestampMs + currentTime,
  56. })
  57. : undefined,
  58. [itemLookup, breadcrumbs, currentTime, startTimestampMs]
  59. );
  60. const hovered = useMemo(
  61. () =>
  62. currentHoverTime && breadcrumbs
  63. ? getPrevReplayEvent({
  64. itemLookup,
  65. items: breadcrumbs,
  66. targetTimestampMs: startTimestampMs + currentHoverTime,
  67. })
  68. : undefined,
  69. [itemLookup, breadcrumbs, currentHoverTime, startTimestampMs]
  70. );
  71. const deps = useMemo(() => [breadcrumbs], [breadcrumbs]);
  72. const {cache, updateList} = useVirtualizedList({
  73. cellMeasurer,
  74. ref: listRef,
  75. deps,
  76. });
  77. const {handleDimensionChange} = useVirtualizedInspector({
  78. cache,
  79. listRef,
  80. expandPathsRef,
  81. });
  82. useScrollToCurrentItem({
  83. breadcrumbs,
  84. ref: listRef,
  85. startTimestampMs,
  86. });
  87. const renderRow = ({index, key, style, parent}: ListRowProps) => {
  88. const item = (breadcrumbs || [])[index];
  89. return (
  90. <CellMeasurer
  91. cache={cache}
  92. columnIndex={0}
  93. key={key}
  94. parent={parent}
  95. rowIndex={index}
  96. >
  97. <BreadcrumbRow
  98. index={index}
  99. isCurrent={current?.id === item.id}
  100. isHovered={hovered?.id === item.id}
  101. breadcrumb={item}
  102. startTimestampMs={startTimestampMs}
  103. style={style}
  104. expandPaths={Array.from(expandPathsRef.current?.get(index) || [])}
  105. onDimensionChange={handleDimensionChange}
  106. />
  107. </CellMeasurer>
  108. );
  109. };
  110. return (
  111. <FluidHeight>
  112. <BreadcrumbContainer>
  113. {breadcrumbs ? (
  114. <AutoSizer onResize={updateList}>
  115. {({height, width}) => (
  116. <ReactVirtualizedList
  117. deferredMeasurementCache={cache}
  118. height={height}
  119. noRowsRenderer={() => (
  120. <NoRowRenderer unfilteredItems={breadcrumbs} clearSearchTerm={() => {}}>
  121. {t('No breadcrumbs recorded')}
  122. </NoRowRenderer>
  123. )}
  124. overscanRowCount={5}
  125. ref={listRef}
  126. rowCount={breadcrumbs.length}
  127. rowHeight={cache.rowHeight}
  128. rowRenderer={renderRow}
  129. width={width}
  130. />
  131. )}
  132. </AutoSizer>
  133. ) : (
  134. <Placeholder height="100%" />
  135. )}
  136. </BreadcrumbContainer>
  137. </FluidHeight>
  138. );
  139. }
  140. const BreadcrumbContainer = styled('div')`
  141. position: relative;
  142. height: 100%;
  143. overflow: hidden;
  144. border: 1px solid ${p => p.theme.border};
  145. border-radius: ${p => p.theme.borderRadius};
  146. `;
  147. export default memo(Breadcrumbs);