index.tsx 6.1 KB

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