Browse Source

feat(replay): Add Jump up|down buttons to all the Replay Details tables & lists (#58359)

Replaces https://github.com/getsentry/sentry/pull/58131
Ryan Albrecht 1 year ago
parent
commit
5700e591f6

+ 31 - 33
static/app/components/replays/useJumpButtons.tsx

@@ -1,65 +1,63 @@
 import {useCallback, useMemo, useState} from 'react';
-import {ScrollParams} from 'react-virtualized';
+import type {IndexRange, SectionRenderedParams} from 'react-virtualized';
 
 import {getNextReplayFrame} from 'sentry/utils/replays/getReplayEvent';
-import {ReplayFrame} from 'sentry/utils/replays/types';
-
-/**
- * The range (`[startIndex, endIndex]`) of table rows that are visible,
- * not including the table header.
- */
-type VisibleRange = [number, number];
+import type {ReplayFrame} from 'sentry/utils/replays/types';
 
 interface Props {
   currentTime: number;
   frames: ReplayFrame[];
-  rowHeight: number;
+  isTable: boolean;
   setScrollToRow: (row: number) => void;
 }
 
 export default function useJumpButtons({
   currentTime,
   frames,
-  rowHeight,
+  isTable,
   setScrollToRow,
 }: Props) {
-  const [visibleRange, setVisibleRange] = useState<VisibleRange>([0, 0]);
+  const [visibleRange, setVisibleRange] = useState<IndexRange>({
+    startIndex: 0,
+    stopIndex: 0,
+  });
 
-  const indexOfCurrentRow = useMemo(() => {
+  const frameIndex = useMemo(() => {
     const frame = getNextReplayFrame({
       frames,
       targetOffsetMs: currentTime,
       allowExact: true,
     });
-    const frameIndex = frames.findIndex(spanFrame => frame === spanFrame);
-    // frameIndex is -1 at end of replay, so use last index
-    const index = frameIndex === -1 ? frames.length - 1 : frameIndex;
-    return index;
+    const index = frames.findIndex(spanFrame => frame === spanFrame);
+    // index is -1 at end of replay, so use last index
+    return index === -1 ? frames.length - 1 : index;
   }, [currentTime, frames]);
 
+  // Tables have a header row, so we need to adjust for that.
+  const rowIndex = isTable ? frameIndex + 1 : frameIndex;
+
   const handleClick = useCallback(() => {
-    // When Jump Down, ensures purple line is visible and index needs to be 1 to jump to top of network list
-    if (indexOfCurrentRow > visibleRange[1] || indexOfCurrentRow === 0) {
-      setScrollToRow(indexOfCurrentRow + 1);
-    } else {
-      setScrollToRow(indexOfCurrentRow);
-    }
-  }, [indexOfCurrentRow, setScrollToRow, visibleRange]);
+    // When Jump Down, ensures purple line is visible and index needs to be 1 to jump to top of the list
+    const jumpDownFurther =
+      isTable && (rowIndex > visibleRange.stopIndex || rowIndex === 0);
+
+    setScrollToRow(rowIndex + (jumpDownFurther ? 1 : 0));
+  }, [isTable, rowIndex, setScrollToRow, visibleRange]);
+
+  const onRowsRendered = setVisibleRange;
 
-  const handleScroll = useCallback(
-    ({clientHeight, scrollTop}: ScrollParams) => {
-      setVisibleRange([
-        Math.floor(scrollTop / rowHeight),
-        Math.floor(scrollTop + clientHeight / rowHeight),
-      ]);
+  const onSectionRendered = useCallback(
+    ({rowStartIndex, rowStopIndex}: SectionRenderedParams) => {
+      setVisibleRange({startIndex: rowStartIndex, stopIndex: rowStopIndex});
     },
-    [rowHeight]
+    []
   );
 
   return {
-    showJumpUpButton: indexOfCurrentRow < visibleRange[0],
-    showJumpDownButton: indexOfCurrentRow > visibleRange[1],
     handleClick,
-    handleScroll,
+    onRowsRendered,
+    onSectionRendered,
+    showJumpDownButton: rowIndex > visibleRange.stopIndex,
+    showJumpUpButton: rowIndex < visibleRange.startIndex,
   };
 }

+ 1 - 1
static/app/utils/replays/hooks/useA11yData.tsx

@@ -1,6 +1,6 @@
 import {useReplayContext} from 'sentry/components/replays/replayContext';
 import {useApiQuery} from 'sentry/utils/queryClient';
-import hydrateA11yFrame, {RawA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
+import hydrateA11yFrame, {RawA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
 import useOrganization from 'sentry/utils/useOrganization';
 import useProjects from 'sentry/utils/useProjects';
 

+ 1 - 1
static/app/utils/replays/hooks/useMockA11yData.tsx

@@ -1,7 +1,7 @@
 import {useEffect, useState} from 'react';
 
 import {useReplayContext} from 'sentry/components/replays/replayContext';
-import hydrateA11yFrame, {RawA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
+import hydrateA11yFrame, {RawA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
 import useProjects from 'sentry/utils/useProjects';
 
 export default function useA11yData() {

+ 0 - 0
static/app/utils/replays/hydrateA11yRecord.tsx → static/app/utils/replays/hydrateA11yFrame.tsx


+ 1 - 1
static/app/utils/replays/types.tsx

@@ -15,7 +15,7 @@ import type {
 } from '@sentry/react';
 import invariant from 'invariant';
 
-import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
+import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
 
 /**
  * Extra breadcrumb types not included in `@sentry/replay`

+ 1 - 1
static/app/views/replays/detail/accessibility/accessibilityTableCell.tsx

@@ -10,7 +10,7 @@ import {
 import {Tooltip} from 'sentry/components/tooltip';
 import {IconFire, IconInfo, IconWarning} from 'sentry/icons';
 import type useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
-import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
+import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
 import {Color} from 'sentry/utils/theme';
 import useUrlParams from 'sentry/utils/useUrlParams';
 import useSortAccessibility from 'sentry/views/replays/detail/accessibility/useSortAccessibility';

+ 23 - 1
static/app/views/replays/detail/accessibility/index.tsx

@@ -3,7 +3,9 @@ import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualiz
 import styled from '@emotion/styled';
 
 import Placeholder from 'sentry/components/placeholder';
+import JumpButtons from 'sentry/components/replays/jumpButtons';
 import {useReplayContext} from 'sentry/components/replays/replayContext';
+import useJumpButtons from 'sentry/components/replays/useJumpButtons';
 import {t} from 'sentry/locale';
 // import useA11yData from 'sentry/utils/replays/hooks/useA11yData';
 import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
@@ -84,6 +86,18 @@ function AccessibilityList() {
       ? Math.min(maxContainerHeight, containerSize)
       : undefined;
 
+  const {
+    handleClick: onClickToJump,
+    onSectionRendered,
+    showJumpDownButton,
+    showJumpUpButton,
+  } = useJumpButtons({
+    currentTime,
+    frames: filteredItems,
+    isTable: true,
+    setScrollToRow,
+  });
+
   const onClickCell = useCallback(
     ({}: {dataIndex: number; rowIndex: number}) => {
       // eslint-disable-line
@@ -186,15 +200,23 @@ function AccessibilityList() {
                         setScrollToRow(undefined);
                       }
                     }}
-                    scrollToRow={scrollToRow}
+                    onSectionRendered={onSectionRendered}
                     overscanColumnCount={COLUMN_COUNT}
                     overscanRowCount={5}
                     rowCount={items.length + 1}
                     rowHeight={({index}) => (index === 0 ? HEADER_HEIGHT : BODY_HEIGHT)}
+                    scrollToRow={scrollToRow}
                     width={width}
                   />
                 )}
               </AutoSizer>
+              {sortConfig.by === 'timestamp' && items.length ? (
+                <JumpButtons
+                  jump={showJumpUpButton ? 'up' : showJumpDownButton ? 'down' : undefined}
+                  onClick={onClickToJump}
+                  tableHeaderHeight={HEADER_HEIGHT}
+                />
+              ) : null}
             </OverflowHidden>
           ) : (
             <Placeholder height="100%" />

+ 1 - 1
static/app/views/replays/detail/accessibility/useAccessibilityFilters.tsx

@@ -3,7 +3,7 @@ import {useCallback, useMemo} from 'react';
 import type {SelectOption} from 'sentry/components/compactSelect';
 import {decodeList, decodeScalar} from 'sentry/utils/queryString';
 import useFiltersInLocationQuery from 'sentry/utils/replays/hooks/useFiltersInLocationQuery';
-import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
+import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
 import {filterItems} from 'sentry/views/replays/detail/utils';
 
 export interface AccessibilitySelectOption extends SelectOption<string> {

+ 1 - 1
static/app/views/replays/detail/accessibility/useSortAccessibility.tsx

@@ -1,6 +1,6 @@
 import {useCallback, useMemo} from 'react';
 
-import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
+import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
 import useUrlParams from 'sentry/utils/useUrlParams';
 
 interface SortConfig {

+ 39 - 22
static/app/views/replays/detail/breadcrumbs/index.tsx

@@ -1,4 +1,4 @@
-import {useMemo, useRef} from 'react';
+import {useMemo, useRef, useState} from 'react';
 import {
   AutoSizer,
   CellMeasurer,
@@ -7,11 +7,11 @@ import {
 } from 'react-virtualized';
 
 import Placeholder from 'sentry/components/placeholder';
+import JumpButtons from 'sentry/components/replays/jumpButtons';
+import {useReplayContext} from 'sentry/components/replays/replayContext';
+import useJumpButtons from 'sentry/components/replays/useJumpButtons';
 import {t} from 'sentry/locale';
-import getFrameDetails from 'sentry/utils/replays/getFrameDetails';
-import useActiveReplayTab from 'sentry/utils/replays/hooks/useActiveReplayTab';
 import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
-import type {ReplayFrame} from 'sentry/utils/replays/types';
 import BreadcrumbFilters from 'sentry/views/replays/detail/breadcrumbs/breadcrumbFilters';
 import BreadcrumbRow from 'sentry/views/replays/detail/breadcrumbs/breadcrumbRow';
 import useBreadcrumbFilters from 'sentry/views/replays/detail/breadcrumbs/useBreadcrumbFilters';
@@ -23,11 +23,6 @@ import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList';
 
 import useVirtualizedInspector from '../useVirtualizedInspector';
 
-type Props = {
-  frames: undefined | ReplayFrame[];
-  startTimestampMs: number;
-};
-
 // Ensure this object is created once as it is an input to
 // `useVirtualizedList`'s memoization
 const cellMeasurer = {
@@ -35,37 +30,46 @@ const cellMeasurer = {
   minHeight: 53,
 };
 
-function Breadcrumbs({frames, startTimestampMs}: Props) {
+function Breadcrumbs() {
+  const {currentTime, replay} = useReplayContext();
   const {onClickTimestamp} = useCrumbHandlers();
 
-  const {setActiveTab} = useActiveReplayTab();
+  const startTimestampMs = replay?.getReplay()?.started_at?.getTime() ?? 0;
+  const frames = replay?.getChapterFrames();
 
-  const listRef = useRef<ReactVirtualizedList>(null);
-  // Keep a reference of object paths that are expanded (via <ObjectInspector>)
-  // by log row, so they they can be restored as the Console pane is scrolling.
-  // Due to virtualization, components can be unmounted as the user scrolls, so
-  // state needs to be remembered.
-  //
-  // Note that this is intentionally not in state because we do not want to
-  // re-render when items are expanded/collapsed, though it may work in state as well.
-  const expandPathsRef = useRef(new Map<number, Set<string>>());
+  const [scrollToRow, setScrollToRow] = useState<undefined | number>(undefined);
 
   const filterProps = useBreadcrumbFilters({frames: frames || []});
-  const {items, searchTerm, setSearchTerm} = filterProps;
+  const {expandPathsRef, items, searchTerm, setSearchTerm} = filterProps;
   const clearSearchTerm = () => setSearchTerm('');
 
+  const listRef = useRef<ReactVirtualizedList>(null);
+
   const deps = useMemo(() => [items, searchTerm], [items, searchTerm]);
   const {cache, updateList} = useVirtualizedList({
     cellMeasurer,
     ref: listRef,
     deps,
   });
+
   const {handleDimensionChange} = useVirtualizedInspector({
     cache,
     listRef,
     expandPathsRef,
   });
 
+  const {
+    handleClick: onClickToJump,
+    onRowsRendered,
+    showJumpDownButton,
+    showJumpUpButton,
+  } = useJumpButtons({
+    currentTime,
+    frames: items,
+    isTable: false,
+    setScrollToRow,
+  });
+
   useScrollToCurrentItem({
     frames,
     ref: listRef,
@@ -90,7 +94,6 @@ function Breadcrumbs({frames, startTimestampMs}: Props) {
           expandPaths={Array.from(expandPathsRef.current?.get(index) || [])}
           onClick={() => {
             onClickTimestamp(item);
-            setActiveTab(getFrameDetails(item).tabKey);
           }}
           onDimensionChange={handleDimensionChange}
         />
@@ -116,11 +119,18 @@ function Breadcrumbs({frames, startTimestampMs}: Props) {
                     {t('No breadcrumbs recorded')}
                   </NoRowRenderer>
                 )}
+                onRowsRendered={onRowsRendered}
+                onScroll={() => {
+                  if (scrollToRow !== undefined) {
+                    setScrollToRow(undefined);
+                  }
+                }}
                 overscanRowCount={5}
                 ref={listRef}
                 rowCount={items.length}
                 rowHeight={cache.rowHeight}
                 rowRenderer={renderRow}
+                scrollToIndex={scrollToRow}
                 width={width}
               />
             )}
@@ -128,6 +138,13 @@ function Breadcrumbs({frames, startTimestampMs}: Props) {
         ) : (
           <Placeholder height="100%" />
         )}
+        {items?.length ? (
+          <JumpButtons
+            jump={showJumpUpButton ? 'up' : showJumpDownButton ? 'down' : undefined}
+            onClick={onClickToJump}
+            tableHeaderHeight={0}
+          />
+        ) : null}
       </TabItemContainer>
     </FluidHeight>
   );

Some files were not shown because too many files changed in this diff