index.tsx 7.0 KB

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