index.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import {memo, 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 {BreadcrumbTypeDefault, Crumb} from 'sentry/types/breadcrumbs';
  13. import {getPrevReplayEvent} from 'sentry/utils/replays/getReplayEvent';
  14. import ConsoleFilters from 'sentry/views/replays/detail/console/consoleFilters';
  15. import ConsoleLogRow from 'sentry/views/replays/detail/console/consoleLogRow';
  16. import useConsoleFilters from 'sentry/views/replays/detail/console/useConsoleFilters';
  17. import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
  18. import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer';
  19. import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList';
  20. interface Props {
  21. breadcrumbs: undefined | Extract<Crumb, BreadcrumbTypeDefault>[];
  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: 24,
  29. };
  30. function Console({breadcrumbs, startTimestampMs}: Props) {
  31. const filterProps = useConsoleFilters({breadcrumbs: breadcrumbs || []});
  32. const {expandPaths, searchTerm, logLevel, items, setSearchTerm} = filterProps;
  33. const clearSearchTerm = () => setSearchTerm('');
  34. const {currentTime, currentHoverTime} = useReplayContext();
  35. const listRef = useRef<ReactVirtualizedList>(null);
  36. const itemLookup = useMemo(
  37. () =>
  38. breadcrumbs &&
  39. breadcrumbs
  40. .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
  41. .sort(([a], [b]) => a - b),
  42. [breadcrumbs]
  43. );
  44. const deps = useMemo(() => [items], [items]);
  45. const {cache, updateList} = useVirtualizedList({
  46. cellMeasurer,
  47. ref: listRef,
  48. deps,
  49. });
  50. const handleDimensionChange = useCallback(
  51. (index: number, path: string, expandedState: Record<string, boolean>) => {
  52. const rowState = expandPaths.get(index) || new Set();
  53. if (expandedState[path]) {
  54. rowState.add(path);
  55. } else {
  56. // Collapsed, i.e. its default state, so no need to store state
  57. rowState.delete(path);
  58. }
  59. expandPaths.set(index, rowState);
  60. cache.clear(index, 0);
  61. listRef.current?.recomputeGridSize({rowIndex: index});
  62. listRef.current?.forceUpdateGrid();
  63. },
  64. [cache, expandPaths, listRef]
  65. );
  66. const current = useMemo(
  67. () =>
  68. breadcrumbs
  69. ? getPrevReplayEvent({
  70. itemLookup,
  71. items: breadcrumbs,
  72. targetTimestampMs: startTimestampMs + currentTime,
  73. })
  74. : undefined,
  75. [itemLookup, breadcrumbs, currentTime, startTimestampMs]
  76. );
  77. const hovered = useMemo(
  78. () =>
  79. currentHoverTime && breadcrumbs
  80. ? getPrevReplayEvent({
  81. itemLookup,
  82. items: breadcrumbs,
  83. targetTimestampMs: startTimestampMs + currentHoverTime,
  84. })
  85. : undefined,
  86. [itemLookup, breadcrumbs, currentHoverTime, startTimestampMs]
  87. );
  88. const renderRow = ({index, key, style, parent}: ListRowProps) => {
  89. const item = items[index];
  90. return (
  91. <CellMeasurer
  92. cache={cache}
  93. columnIndex={0}
  94. // Set key based on filters, otherwise we can have odd expand/collapse state
  95. // with <ObjectInspector> when filtering
  96. key={`${searchTerm}-${logLevel.join(',')}-${key}`}
  97. parent={parent}
  98. rowIndex={index}
  99. >
  100. <ConsoleLogRow
  101. isCurrent={current?.id === item.id}
  102. isHovered={hovered?.id === item.id}
  103. breadcrumb={item}
  104. index={index}
  105. startTimestampMs={startTimestampMs}
  106. style={style}
  107. expandPaths={Array.from(expandPaths.get(index) || [])}
  108. onDimensionChange={handleDimensionChange}
  109. />
  110. </CellMeasurer>
  111. );
  112. };
  113. return (
  114. <FluidHeight>
  115. <ConsoleFilters breadcrumbs={breadcrumbs} {...filterProps} />
  116. <ConsoleLogContainer>
  117. {breadcrumbs ? (
  118. <AutoSizer onResize={updateList}>
  119. {({width, height}) => (
  120. <ReactVirtualizedList
  121. deferredMeasurementCache={cache}
  122. height={height}
  123. noRowsRenderer={() => (
  124. <NoRowRenderer
  125. unfilteredItems={breadcrumbs}
  126. clearSearchTerm={clearSearchTerm}
  127. >
  128. {t('No console logs recorded')}
  129. </NoRowRenderer>
  130. )}
  131. overscanRowCount={5}
  132. ref={listRef}
  133. rowCount={items.length}
  134. rowHeight={cache.rowHeight}
  135. rowRenderer={renderRow}
  136. width={width}
  137. />
  138. )}
  139. </AutoSizer>
  140. ) : (
  141. <Placeholder height="100%" />
  142. )}
  143. </ConsoleLogContainer>
  144. </FluidHeight>
  145. );
  146. }
  147. const ConsoleLogContainer = styled('div')`
  148. position: relative;
  149. height: 100%;
  150. overflow: hidden;
  151. border: 1px solid ${p => p.theme.border};
  152. border-radius: ${p => p.theme.borderRadius};
  153. `;
  154. export default memo(Console);