Skip to content

Commit 5700e59

Browse files
authored
feat(replay): Add Jump up|down buttons to all the Replay Details tables & lists (#58359)
Replaces #58131
1 parent e9df130 commit 5700e59

File tree

15 files changed

+158
-96
lines changed

15 files changed

+158
-96
lines changed

Diff for: static/app/components/replays/useJumpButtons.tsx

+31-33
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,63 @@
11
import {useCallback, useMemo, useState} from 'react';
2-
import {ScrollParams} from 'react-virtualized';
2+
import type {IndexRange, SectionRenderedParams} from 'react-virtualized';
33

44
import {getNextReplayFrame} from 'sentry/utils/replays/getReplayEvent';
5-
import {ReplayFrame} from 'sentry/utils/replays/types';
6-
7-
/**
8-
* The range (`[startIndex, endIndex]`) of table rows that are visible,
9-
* not including the table header.
10-
*/
11-
type VisibleRange = [number, number];
5+
import type {ReplayFrame} from 'sentry/utils/replays/types';
126

137
interface Props {
148
currentTime: number;
159
frames: ReplayFrame[];
16-
rowHeight: number;
10+
isTable: boolean;
1711
setScrollToRow: (row: number) => void;
1812
}
1913

2014
export default function useJumpButtons({
2115
currentTime,
2216
frames,
23-
rowHeight,
17+
isTable,
2418
setScrollToRow,
2519
}: Props) {
26-
const [visibleRange, setVisibleRange] = useState<VisibleRange>([0, 0]);
20+
const [visibleRange, setVisibleRange] = useState<IndexRange>({
21+
startIndex: 0,
22+
stopIndex: 0,
23+
});
2724

28-
const indexOfCurrentRow = useMemo(() => {
25+
const frameIndex = useMemo(() => {
2926
const frame = getNextReplayFrame({
3027
frames,
3128
targetOffsetMs: currentTime,
3229
allowExact: true,
3330
});
34-
const frameIndex = frames.findIndex(spanFrame => frame === spanFrame);
35-
// frameIndex is -1 at end of replay, so use last index
36-
const index = frameIndex === -1 ? frames.length - 1 : frameIndex;
37-
return index;
31+
const index = frames.findIndex(spanFrame => frame === spanFrame);
32+
// index is -1 at end of replay, so use last index
33+
return index === -1 ? frames.length - 1 : index;
3834
}, [currentTime, frames]);
3935

36+
// Tables have a header row, so we need to adjust for that.
37+
const rowIndex = isTable ? frameIndex + 1 : frameIndex;
38+
4039
const handleClick = useCallback(() => {
41-
// When Jump Down, ensures purple line is visible and index needs to be 1 to jump to top of network list
42-
if (indexOfCurrentRow > visibleRange[1] || indexOfCurrentRow === 0) {
43-
setScrollToRow(indexOfCurrentRow + 1);
44-
} else {
45-
setScrollToRow(indexOfCurrentRow);
46-
}
47-
}, [indexOfCurrentRow, setScrollToRow, visibleRange]);
40+
// When Jump Down, ensures purple line is visible and index needs to be 1 to jump to top of the list
41+
const jumpDownFurther =
42+
isTable && (rowIndex > visibleRange.stopIndex || rowIndex === 0);
43+
44+
setScrollToRow(rowIndex + (jumpDownFurther ? 1 : 0));
45+
}, [isTable, rowIndex, setScrollToRow, visibleRange]);
46+
47+
const onRowsRendered = setVisibleRange;
4848

49-
const handleScroll = useCallback(
50-
({clientHeight, scrollTop}: ScrollParams) => {
51-
setVisibleRange([
52-
Math.floor(scrollTop / rowHeight),
53-
Math.floor(scrollTop + clientHeight / rowHeight),
54-
]);
49+
const onSectionRendered = useCallback(
50+
({rowStartIndex, rowStopIndex}: SectionRenderedParams) => {
51+
setVisibleRange({startIndex: rowStartIndex, stopIndex: rowStopIndex});
5552
},
56-
[rowHeight]
53+
[]
5754
);
5855

5956
return {
60-
showJumpUpButton: indexOfCurrentRow < visibleRange[0],
61-
showJumpDownButton: indexOfCurrentRow > visibleRange[1],
6257
handleClick,
63-
handleScroll,
58+
onRowsRendered,
59+
onSectionRendered,
60+
showJumpDownButton: rowIndex > visibleRange.stopIndex,
61+
showJumpUpButton: rowIndex < visibleRange.startIndex,
6462
};
6563
}

Diff for: static/app/utils/replays/hooks/useA11yData.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {useReplayContext} from 'sentry/components/replays/replayContext';
22
import {useApiQuery} from 'sentry/utils/queryClient';
3-
import hydrateA11yFrame, {RawA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
3+
import hydrateA11yFrame, {RawA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
44
import useOrganization from 'sentry/utils/useOrganization';
55
import useProjects from 'sentry/utils/useProjects';
66

Diff for: static/app/utils/replays/hooks/useMockA11yData.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {useEffect, useState} from 'react';
22

33
import {useReplayContext} from 'sentry/components/replays/replayContext';
4-
import hydrateA11yFrame, {RawA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
4+
import hydrateA11yFrame, {RawA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
55
import useProjects from 'sentry/utils/useProjects';
66

77
export default function useA11yData() {

Diff for: static/app/utils/replays/types.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
} from '@sentry/react';
1616
import invariant from 'invariant';
1717

18-
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
18+
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
1919

2020
/**
2121
* Extra breadcrumb types not included in `@sentry/replay`

Diff for: static/app/views/replays/detail/accessibility/accessibilityTableCell.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import {Tooltip} from 'sentry/components/tooltip';
1111
import {IconFire, IconInfo, IconWarning} from 'sentry/icons';
1212
import type useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
13-
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
13+
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
1414
import {Color} from 'sentry/utils/theme';
1515
import useUrlParams from 'sentry/utils/useUrlParams';
1616
import useSortAccessibility from 'sentry/views/replays/detail/accessibility/useSortAccessibility';

Diff for: static/app/views/replays/detail/accessibility/index.tsx

+23-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualiz
33
import styled from '@emotion/styled';
44

55
import Placeholder from 'sentry/components/placeholder';
6+
import JumpButtons from 'sentry/components/replays/jumpButtons';
67
import {useReplayContext} from 'sentry/components/replays/replayContext';
8+
import useJumpButtons from 'sentry/components/replays/useJumpButtons';
79
import {t} from 'sentry/locale';
810
// import useA11yData from 'sentry/utils/replays/hooks/useA11yData';
911
import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
@@ -84,6 +86,18 @@ function AccessibilityList() {
8486
? Math.min(maxContainerHeight, containerSize)
8587
: undefined;
8688

89+
const {
90+
handleClick: onClickToJump,
91+
onSectionRendered,
92+
showJumpDownButton,
93+
showJumpUpButton,
94+
} = useJumpButtons({
95+
currentTime,
96+
frames: filteredItems,
97+
isTable: true,
98+
setScrollToRow,
99+
});
100+
87101
const onClickCell = useCallback(
88102
({}: {dataIndex: number; rowIndex: number}) => {
89103
// eslint-disable-line
@@ -186,15 +200,23 @@ function AccessibilityList() {
186200
setScrollToRow(undefined);
187201
}
188202
}}
189-
scrollToRow={scrollToRow}
203+
onSectionRendered={onSectionRendered}
190204
overscanColumnCount={COLUMN_COUNT}
191205
overscanRowCount={5}
192206
rowCount={items.length + 1}
193207
rowHeight={({index}) => (index === 0 ? HEADER_HEIGHT : BODY_HEIGHT)}
208+
scrollToRow={scrollToRow}
194209
width={width}
195210
/>
196211
)}
197212
</AutoSizer>
213+
{sortConfig.by === 'timestamp' && items.length ? (
214+
<JumpButtons
215+
jump={showJumpUpButton ? 'up' : showJumpDownButton ? 'down' : undefined}
216+
onClick={onClickToJump}
217+
tableHeaderHeight={HEADER_HEIGHT}
218+
/>
219+
) : null}
198220
</OverflowHidden>
199221
) : (
200222
<Placeholder height="100%" />

Diff for: static/app/views/replays/detail/accessibility/useAccessibilityFilters.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {useCallback, useMemo} from 'react';
33
import type {SelectOption} from 'sentry/components/compactSelect';
44
import {decodeList, decodeScalar} from 'sentry/utils/queryString';
55
import useFiltersInLocationQuery from 'sentry/utils/replays/hooks/useFiltersInLocationQuery';
6-
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
6+
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
77
import {filterItems} from 'sentry/views/replays/detail/utils';
88

99
export interface AccessibilitySelectOption extends SelectOption<string> {

Diff for: static/app/views/replays/detail/accessibility/useSortAccessibility.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {useCallback, useMemo} from 'react';
22

3-
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yRecord';
3+
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
44
import useUrlParams from 'sentry/utils/useUrlParams';
55

66
interface SortConfig {

Diff for: static/app/views/replays/detail/breadcrumbs/index.tsx

+39-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useMemo, useRef} from 'react';
1+
import {useMemo, useRef, useState} from 'react';
22
import {
33
AutoSizer,
44
CellMeasurer,
@@ -7,11 +7,11 @@ import {
77
} from 'react-virtualized';
88

99
import Placeholder from 'sentry/components/placeholder';
10+
import JumpButtons from 'sentry/components/replays/jumpButtons';
11+
import {useReplayContext} from 'sentry/components/replays/replayContext';
12+
import useJumpButtons from 'sentry/components/replays/useJumpButtons';
1013
import {t} from 'sentry/locale';
11-
import getFrameDetails from 'sentry/utils/replays/getFrameDetails';
12-
import useActiveReplayTab from 'sentry/utils/replays/hooks/useActiveReplayTab';
1314
import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
14-
import type {ReplayFrame} from 'sentry/utils/replays/types';
1515
import BreadcrumbFilters from 'sentry/views/replays/detail/breadcrumbs/breadcrumbFilters';
1616
import BreadcrumbRow from 'sentry/views/replays/detail/breadcrumbs/breadcrumbRow';
1717
import useBreadcrumbFilters from 'sentry/views/replays/detail/breadcrumbs/useBreadcrumbFilters';
@@ -23,49 +23,53 @@ import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList';
2323

2424
import useVirtualizedInspector from '../useVirtualizedInspector';
2525

26-
type Props = {
27-
frames: undefined | ReplayFrame[];
28-
startTimestampMs: number;
29-
};
30-
3126
// Ensure this object is created once as it is an input to
3227
// `useVirtualizedList`'s memoization
3328
const cellMeasurer = {
3429
fixedWidth: true,
3530
minHeight: 53,
3631
};
3732

38-
function Breadcrumbs({frames, startTimestampMs}: Props) {
33+
function Breadcrumbs() {
34+
const {currentTime, replay} = useReplayContext();
3935
const {onClickTimestamp} = useCrumbHandlers();
4036

41-
const {setActiveTab} = useActiveReplayTab();
37+
const startTimestampMs = replay?.getReplay()?.started_at?.getTime() ?? 0;
38+
const frames = replay?.getChapterFrames();
4239

43-
const listRef = useRef<ReactVirtualizedList>(null);
44-
// Keep a reference of object paths that are expanded (via <ObjectInspector>)
45-
// by log row, so they they can be restored as the Console pane is scrolling.
46-
// Due to virtualization, components can be unmounted as the user scrolls, so
47-
// state needs to be remembered.
48-
//
49-
// Note that this is intentionally not in state because we do not want to
50-
// re-render when items are expanded/collapsed, though it may work in state as well.
51-
const expandPathsRef = useRef(new Map<number, Set<string>>());
40+
const [scrollToRow, setScrollToRow] = useState<undefined | number>(undefined);
5241

5342
const filterProps = useBreadcrumbFilters({frames: frames || []});
54-
const {items, searchTerm, setSearchTerm} = filterProps;
43+
const {expandPathsRef, items, searchTerm, setSearchTerm} = filterProps;
5544
const clearSearchTerm = () => setSearchTerm('');
5645

46+
const listRef = useRef<ReactVirtualizedList>(null);
47+
5748
const deps = useMemo(() => [items, searchTerm], [items, searchTerm]);
5849
const {cache, updateList} = useVirtualizedList({
5950
cellMeasurer,
6051
ref: listRef,
6152
deps,
6253
});
54+
6355
const {handleDimensionChange} = useVirtualizedInspector({
6456
cache,
6557
listRef,
6658
expandPathsRef,
6759
});
6860

61+
const {
62+
handleClick: onClickToJump,
63+
onRowsRendered,
64+
showJumpDownButton,
65+
showJumpUpButton,
66+
} = useJumpButtons({
67+
currentTime,
68+
frames: items,
69+
isTable: false,
70+
setScrollToRow,
71+
});
72+
6973
useScrollToCurrentItem({
7074
frames,
7175
ref: listRef,
@@ -90,7 +94,6 @@ function Breadcrumbs({frames, startTimestampMs}: Props) {
9094
expandPaths={Array.from(expandPathsRef.current?.get(index) || [])}
9195
onClick={() => {
9296
onClickTimestamp(item);
93-
setActiveTab(getFrameDetails(item).tabKey);
9497
}}
9598
onDimensionChange={handleDimensionChange}
9699
/>
@@ -116,18 +119,32 @@ function Breadcrumbs({frames, startTimestampMs}: Props) {
116119
{t('No breadcrumbs recorded')}
117120
</NoRowRenderer>
118121
)}
122+
onRowsRendered={onRowsRendered}
123+
onScroll={() => {
124+
if (scrollToRow !== undefined) {
125+
setScrollToRow(undefined);
126+
}
127+
}}
119128
overscanRowCount={5}
120129
ref={listRef}
121130
rowCount={items.length}
122131
rowHeight={cache.rowHeight}
123132
rowRenderer={renderRow}
133+
scrollToIndex={scrollToRow}
124134
width={width}
125135
/>
126136
)}
127137
</AutoSizer>
128138
) : (
129139
<Placeholder height="100%" />
130140
)}
141+
{items?.length ? (
142+
<JumpButtons
143+
jump={showJumpUpButton ? 'up' : showJumpDownButton ? 'down' : undefined}
144+
onClick={onClickToJump}
145+
tableHeaderHeight={0}
146+
/>
147+
) : null}
131148
</TabItemContainer>
132149
</FluidHeight>
133150
);

Diff for: static/app/views/replays/detail/breadcrumbs/useBreadcrumbFilters.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useCallback, useMemo} from 'react';
1+
import {RefObject, useCallback, useMemo, useRef} from 'react';
22
import uniq from 'lodash/uniq';
33

44
import {decodeList, decodeScalar} from 'sentry/utils/queryString';
@@ -16,6 +16,7 @@ type Options = {
1616
};
1717

1818
type Return = {
19+
expandPathsRef: RefObject<Map<number, Set<string>>>;
1920
getBreadcrumbTypes: () => {label: string; value: string}[];
2021
items: ReplayFrame[];
2122
searchTerm: string;
@@ -81,6 +82,15 @@ const FILTERS = {
8182
function useBreadcrumbFilters({frames}: Options): Return {
8283
const {setFilter, query} = useFiltersInLocationQuery<FilterFields>();
8384

85+
// Keep a reference of object paths that are expanded (via <ObjectInspector>)
86+
// by log row, so they they can be restored as the Console pane is scrolling.
87+
// Due to virtualization, components can be unmounted as the user scrolls, so
88+
// state needs to be remembered.
89+
//
90+
// Note that this is intentionally not in state because we do not want to
91+
// re-render when items are expanded/collapsed, though it may work in state as well.
92+
const expandPathsRef = useRef(new Map<number, Set<string>>());
93+
8494
const type = useMemo(() => decodeList(query.f_b_type), [query.f_b_type]);
8595
const searchTerm = decodeScalar(query.f_b_search, '').toLowerCase();
8696

@@ -125,6 +135,7 @@ function useBreadcrumbFilters({frames}: Options): Return {
125135
);
126136

127137
return {
138+
expandPathsRef,
128139
getBreadcrumbTypes,
129140
items,
130141
searchTerm,

0 commit comments

Comments
 (0)