index.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {useMemo, useRef, useState} from 'react';
  2. import type {GridCellProps} from 'react-virtualized';
  3. import {AutoSizer, CellMeasurer, MultiGrid} from 'react-virtualized';
  4. import styled from '@emotion/styled';
  5. import Placeholder from 'sentry/components/placeholder';
  6. import JumpButtons from 'sentry/components/replays/jumpButtons';
  7. import {useReplayContext} from 'sentry/components/replays/replayContext';
  8. import useJumpButtons from 'sentry/components/replays/useJumpButtons';
  9. import {t} from 'sentry/locale';
  10. import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
  11. import useCurrentHoverTime from 'sentry/utils/replays/playback/providers/useCurrentHoverTime';
  12. import ErrorFilters from 'sentry/views/replays/detail/errorList/errorFilters';
  13. import ErrorHeaderCell, {
  14. COLUMN_COUNT,
  15. } from 'sentry/views/replays/detail/errorList/errorHeaderCell';
  16. import ErrorTableCell from 'sentry/views/replays/detail/errorList/errorTableCell';
  17. import useErrorFilters from 'sentry/views/replays/detail/errorList/useErrorFilters';
  18. import useSortErrors from 'sentry/views/replays/detail/errorList/useSortErrors';
  19. import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
  20. import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer';
  21. import useVirtualizedGrid from 'sentry/views/replays/detail/useVirtualizedGrid';
  22. const HEADER_HEIGHT = 25;
  23. const BODY_HEIGHT = 25;
  24. const cellMeasurer = {
  25. defaultHeight: BODY_HEIGHT,
  26. defaultWidth: 100,
  27. fixedHeight: true,
  28. };
  29. function ErrorList() {
  30. const {currentTime, replay} = useReplayContext();
  31. const [currentHoverTime] = useCurrentHoverTime();
  32. const {onMouseEnter, onMouseLeave, onClickTimestamp} = useCrumbHandlers();
  33. const errorFrames = replay?.getErrorFrames();
  34. const startTimestampMs = replay?.getReplay().started_at.getTime() ?? 0;
  35. const [scrollToRow, setScrollToRow] = useState<undefined | number>(undefined);
  36. const filterProps = useErrorFilters({errorFrames: errorFrames || []});
  37. const {items: filteredItems, searchTerm, setSearchTerm} = filterProps;
  38. const clearSearchTerm = () => setSearchTerm('');
  39. const {handleSort, items, sortConfig} = useSortErrors({items: filteredItems});
  40. const gridRef = useRef<MultiGrid>(null);
  41. const deps = useMemo(() => [items, searchTerm], [items, searchTerm]);
  42. const {cache, getColumnWidth, onScrollbarPresenceChange, onWrapperResize} =
  43. useVirtualizedGrid({
  44. cellMeasurer,
  45. gridRef,
  46. columnCount: COLUMN_COUNT,
  47. dynamicColumnIndex: 1,
  48. deps,
  49. });
  50. const {
  51. handleClick: onClickToJump,
  52. onSectionRendered,
  53. showJumpDownButton,
  54. showJumpUpButton,
  55. } = useJumpButtons({
  56. currentTime,
  57. frames: filteredItems,
  58. isTable: true,
  59. setScrollToRow,
  60. });
  61. const cellRenderer = ({columnIndex, rowIndex, key, style, parent}: GridCellProps) => {
  62. const error = items[rowIndex - 1];
  63. return (
  64. <CellMeasurer
  65. cache={cache}
  66. columnIndex={columnIndex}
  67. key={key}
  68. parent={parent}
  69. rowIndex={rowIndex}
  70. >
  71. {({
  72. measure: _,
  73. registerChild,
  74. }: {
  75. measure: () => void;
  76. registerChild?: (element?: Element) => void;
  77. }) =>
  78. rowIndex === 0 ? (
  79. <ErrorHeaderCell
  80. ref={e => e && registerChild?.(e)}
  81. handleSort={handleSort}
  82. index={columnIndex}
  83. sortConfig={sortConfig}
  84. style={{...style, height: HEADER_HEIGHT}}
  85. />
  86. ) : (
  87. <ErrorTableCell
  88. columnIndex={columnIndex}
  89. currentHoverTime={currentHoverTime}
  90. currentTime={currentTime}
  91. frame={error}
  92. onMouseEnter={onMouseEnter}
  93. onMouseLeave={onMouseLeave}
  94. onClickTimestamp={onClickTimestamp}
  95. ref={e => e && registerChild?.(e)}
  96. rowIndex={rowIndex}
  97. sortConfig={sortConfig}
  98. startTimestampMs={startTimestampMs}
  99. style={{...style, height: BODY_HEIGHT}}
  100. />
  101. )
  102. }
  103. </CellMeasurer>
  104. );
  105. };
  106. return (
  107. <FluidHeight>
  108. <ErrorFilters errorFrames={errorFrames} {...filterProps} />
  109. <ErrorTable data-test-id="replay-details-errors-tab">
  110. {errorFrames ? (
  111. <OverflowHidden>
  112. <AutoSizer onResize={onWrapperResize}>
  113. {({height, width}) => (
  114. <MultiGrid
  115. ref={gridRef}
  116. cellRenderer={cellRenderer}
  117. columnCount={COLUMN_COUNT}
  118. columnWidth={getColumnWidth(width)}
  119. deferredMeasurementCache={cache}
  120. estimatedColumnSize={100}
  121. estimatedRowSize={BODY_HEIGHT}
  122. fixedRowCount={1}
  123. height={height}
  124. noContentRenderer={() => (
  125. <NoRowRenderer
  126. unfilteredItems={errorFrames}
  127. clearSearchTerm={clearSearchTerm}
  128. >
  129. {t('No errors! Go make some.')}
  130. </NoRowRenderer>
  131. )}
  132. onScrollbarPresenceChange={onScrollbarPresenceChange}
  133. onScroll={() => {
  134. if (scrollToRow !== undefined) {
  135. setScrollToRow(undefined);
  136. }
  137. }}
  138. onSectionRendered={onSectionRendered}
  139. overscanColumnCount={COLUMN_COUNT}
  140. overscanRowCount={5}
  141. rowCount={items.length + 1}
  142. rowHeight={({index}) => (index === 0 ? HEADER_HEIGHT : BODY_HEIGHT)}
  143. scrollToRow={scrollToRow}
  144. width={width}
  145. />
  146. )}
  147. </AutoSizer>
  148. {sortConfig.by === 'timestamp' && items.length ? (
  149. <JumpButtons
  150. jump={showJumpUpButton ? 'up' : showJumpDownButton ? 'down' : undefined}
  151. onClick={onClickToJump}
  152. tableHeaderHeight={HEADER_HEIGHT}
  153. />
  154. ) : null}
  155. </OverflowHidden>
  156. ) : (
  157. <Placeholder height="100%" />
  158. )}
  159. </ErrorTable>
  160. </FluidHeight>
  161. );
  162. }
  163. const OverflowHidden = styled('div')`
  164. position: relative;
  165. height: 100%;
  166. overflow: hidden;
  167. display: grid;
  168. `;
  169. const ErrorTable = styled(FluidHeight)`
  170. border: 1px solid ${p => p.theme.border};
  171. border-radius: ${p => p.theme.borderRadius};
  172. .beforeHoverTime + .afterHoverTime:before {
  173. border-top: 1px solid ${p => p.theme.purple200};
  174. content: '';
  175. left: 0;
  176. position: absolute;
  177. top: 0;
  178. width: 999999999%;
  179. }
  180. .beforeHoverTime:last-child:before {
  181. border-bottom: 1px solid ${p => p.theme.purple200};
  182. content: '';
  183. right: 0;
  184. position: absolute;
  185. bottom: 0;
  186. width: 999999999%;
  187. }
  188. .beforeCurrentTime + .afterCurrentTime:before {
  189. border-top: 1px solid ${p => p.theme.purple300};
  190. content: '';
  191. left: 0;
  192. position: absolute;
  193. top: 0;
  194. width: 999999999%;
  195. }
  196. .beforeCurrentTime:last-child:before {
  197. border-bottom: 1px solid ${p => p.theme.purple300};
  198. content: '';
  199. right: 0;
  200. position: absolute;
  201. bottom: 0;
  202. width: 999999999%;
  203. }
  204. `;
  205. export default ErrorList;