Skip to content

Commit e73096b

Browse files
authored
ref(replay): Convert ReplayTable to use SimpleTable under the hood (#94590)
There's some small small differences related to padding and stuff of the table rows, the header row is a bit different which makes column widths different. Overall we're using the same table-cell implementation as before, so all the data rendering is the same. Dropdowns also all moved to the right of the cells, no more shifting left/right depending on which column we're looking at: | Dropdown Example 1 | Dropdown Example 2 | | --- | --- | | <img width="527" alt="SCR-20250627-ojly" src="https://github.com/user-attachments/assets/ebd5a7f6-eca4-4b37-84de-1183c1781af8" /> | <img width="528" alt="SCR-20250627-ojnh" src="https://github.com/user-attachments/assets/3eb0a402-5a91-42d1-b510-dadd013d979c" /> | Page | Before | After | | --- | --- | --- | | Replay List | <img width="1225" alt="SCR-20250627-ohpj" src="https://github.com/user-attachments/assets/cc696b62-3ab5-41fa-8549-3488fc795a4f" /> | <img width="1225" alt="SCR-20250627-ohxj" src="https://github.com/user-attachments/assets/00d076b3-02da-4370-b6a2-0346072e3b4c" /> | Issues -> Replay | <img width="1225" alt="SCR-20250627-ohsv" src="https://github.com/user-attachments/assets/c41d13af-d884-4895-9d6b-861a87c0e32b" /> | <img width="1225" alt="SCR-20250627-oibu" src="https://github.com/user-attachments/assets/188d681e-a3a3-4368-a601-9756593dc104" /> | Transaction -> Replay | <img width="1225" alt="SCR-20250627-ohva" src="https://github.com/user-attachments/assets/30bafef6-5d99-4d7e-8757-8e6271a3fb3d" /> | <img width="1225" alt="SCR-20250627-oide" src="https://github.com/user-attachments/assets/cc12ae96-c4c0-4707-8e21-1c61d6d5448e" /> Fixes REPLAY-460
1 parent c1471f1 commit e73096b

File tree

20 files changed

+496
-846
lines changed

20 files changed

+496
-846
lines changed

static/app/components/events/eventReplay/replayPreviewPlayer.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {ReplayFullscreenButton} from 'sentry/components/replays/replayFullscreen
1313
import ReplayPlayer from 'sentry/components/replays/replayPlayer';
1414
import ReplayPlayPauseButton from 'sentry/components/replays/replayPlayPauseButton';
1515
import {ReplaySidebarToggleButton} from 'sentry/components/replays/replaySidebarToggleButton';
16+
import {ReplaySessionColumn} from 'sentry/components/replays/table/replayTableColumns';
1617
import TimeAndScrubberGrid from 'sentry/components/replays/timeAndScrubberGrid';
1718
import {IconNext, IconPrevious} from 'sentry/icons';
1819
import {t} from 'sentry/locale';
@@ -29,7 +30,6 @@ import Breadcrumbs from 'sentry/views/replays/detail/breadcrumbs';
2930
import BrowserOSIcons from 'sentry/views/replays/detail/browserOSIcons';
3031
import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
3132
import {makeReplaysPathname} from 'sentry/views/replays/pathnames';
32-
import {ReplayCell} from 'sentry/views/replays/replayTable/tableCell';
3333
import type {ReplayListRecord, ReplayRecord} from 'sentry/views/replays/types';
3434

3535
export default function ReplayPreviewPlayer({
@@ -93,10 +93,11 @@ export default function ReplayPreviewPlayer({
9393
</StyledAlert>
9494
)}
9595
<HeaderWrapper>
96-
<StyledReplayCell
97-
key="session"
96+
<ReplaySessionColumn.Component
9897
replay={replayRecord as ReplayListRecord}
9998
rowIndex={0}
99+
columnIndex={0}
100+
showDropdownFilters={false}
100101
/>
101102
<LinkButton
102103
size="sm"
@@ -242,10 +243,6 @@ const ContextContainer = styled('div')`
242243
gap: ${space(1)};
243244
`;
244245

245-
const StyledReplayCell = styled(ReplayCell)`
246-
padding: 0 0 ${space(1)};
247-
`;
248-
249246
const HeaderWrapper = styled('div')`
250247
display: flex;
251248
justify-content: space-between;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import type {HTMLAttributes, ReactNode} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {Alert} from 'sentry/components/core/alert';
5+
import InteractionStateLayer from 'sentry/components/core/interactionStateLayer';
6+
import {Tooltip} from 'sentry/components/core/tooltip';
7+
import LoadingIndicator from 'sentry/components/loadingIndicator';
8+
import type {ReplayTableColumn} from 'sentry/components/replays/table/replayTableColumns';
9+
import {ReplaySessionColumn} from 'sentry/components/replays/table/replayTableColumns';
10+
import {SimpleTable} from 'sentry/components/tables/simpleTable';
11+
import {t} from 'sentry/locale';
12+
import type {Sort} from 'sentry/utils/discover/fields';
13+
import type RequestError from 'sentry/utils/requestError/requestError';
14+
import {ERROR_MAP} from 'sentry/utils/requestError/requestError';
15+
import type {ReplayListRecord} from 'sentry/views/replays/types';
16+
17+
type SortProps =
18+
| {
19+
onSortClick: (key: string) => void;
20+
sort: Sort;
21+
}
22+
| {onSortClick?: never; sort?: never};
23+
24+
type Props = SortProps & {
25+
columns: readonly ReplayTableColumn[];
26+
error: RequestError | null | undefined;
27+
isPending: boolean;
28+
replays: ReplayListRecord[];
29+
showDropdownFilters: boolean;
30+
onClickRow?: (props: {replay: ReplayListRecord; rowIndex: number}) => void;
31+
};
32+
33+
export default function ReplayTable({
34+
columns,
35+
error,
36+
isPending,
37+
onClickRow,
38+
onSortClick,
39+
replays,
40+
showDropdownFilters,
41+
sort,
42+
}: Props) {
43+
if (isPending) {
44+
return (
45+
<ReplayTableWithColumns
46+
data-test-id="replay-table-loading"
47+
columns={columns}
48+
sort={sort}
49+
onSortClick={onSortClick}
50+
>
51+
<SimpleTable.Empty>
52+
<LoadingIndicator />
53+
</SimpleTable.Empty>
54+
</ReplayTableWithColumns>
55+
);
56+
}
57+
58+
if (error) {
59+
return (
60+
<ReplayTableWithColumns
61+
data-test-id="replay-table-errored"
62+
columns={columns}
63+
sort={sort}
64+
onSortClick={onSortClick}
65+
>
66+
<SimpleTable.Empty>
67+
<Alert type="error" showIcon>
68+
{t('Sorry, the list of replays could not be loaded. ')}
69+
{getErrorMessage(error)}
70+
</Alert>
71+
</SimpleTable.Empty>
72+
</ReplayTableWithColumns>
73+
);
74+
}
75+
76+
return (
77+
<ReplayTableWithColumns
78+
data-test-id="replay-table"
79+
columns={columns}
80+
sort={sort}
81+
onSortClick={onSortClick}
82+
>
83+
{replays.length === 0 && (
84+
<SimpleTable.Empty>{t('No replays found')}</SimpleTable.Empty>
85+
)}
86+
{replays.map((replay, rowIndex) => {
87+
const rows = columns.map((column, columnIndex) => (
88+
<RowCell key={`${replay.id}-${column.sortKey}`}>
89+
<column.Component
90+
columnIndex={columnIndex}
91+
replay={replay}
92+
rowIndex={rowIndex}
93+
showDropdownFilters={showDropdownFilters}
94+
/>
95+
</RowCell>
96+
));
97+
return (
98+
<SimpleTable.Row
99+
key={replay.id}
100+
variant={replay.is_archived ? 'faded' : 'default'}
101+
>
102+
{onClickRow ? (
103+
<RowContentButton as="div" onClick={() => onClickRow({replay, rowIndex})}>
104+
<InteractionStateLayer />
105+
{rows}
106+
</RowContentButton>
107+
) : (
108+
rows
109+
)}
110+
</SimpleTable.Row>
111+
);
112+
})}
113+
</ReplayTableWithColumns>
114+
);
115+
}
116+
117+
type TableProps = {
118+
children: ReactNode;
119+
columns: readonly ReplayTableColumn[];
120+
onSortClick?: (key: string) => void;
121+
sort?: Sort;
122+
} & HTMLAttributes<HTMLTableElement>;
123+
124+
const ReplayTableWithColumns = styled(
125+
({children, columns, onSortClick, sort, ...props}: TableProps) => (
126+
<SimpleTable {...props}>
127+
<SimpleTable.Header>
128+
{columns.map((column, columnIndex) => (
129+
<SimpleTable.HeaderCell
130+
key={`${column.name}-${columnIndex}`}
131+
handleSortClick={() => column.sortKey && onSortClick?.(column.sortKey)}
132+
sort={
133+
column.sortKey && sort?.field === column.sortKey ? sort.kind : undefined
134+
}
135+
>
136+
<Tooltip title={column.tooltip} disabled={!column.tooltip}>
137+
{column.name}
138+
</Tooltip>
139+
</SimpleTable.HeaderCell>
140+
))}
141+
</SimpleTable.Header>
142+
143+
{children}
144+
</SimpleTable>
145+
)
146+
)`
147+
${p => getGridTemplateColumns(p.columns)}
148+
margin-bottom: 0;
149+
overflow: auto;
150+
151+
[data-clickable='true'] {
152+
cursor: pointer;
153+
}
154+
`;
155+
156+
function getGridTemplateColumns(columns: readonly ReplayTableColumn[]) {
157+
return `grid-template-columns: ${columns
158+
.map(column =>
159+
column === ReplaySessionColumn ? 'minmax(150px, 1fr)' : 'max-content'
160+
)
161+
.join(' ')};`;
162+
}
163+
164+
function getErrorMessage(fetchError: RequestError) {
165+
if (typeof fetchError === 'string') {
166+
return fetchError;
167+
}
168+
if (typeof fetchError?.responseJSON?.detail === 'string') {
169+
return fetchError.responseJSON.detail;
170+
}
171+
if (fetchError?.responseJSON?.detail?.message) {
172+
return fetchError.responseJSON.detail.message;
173+
}
174+
if (fetchError.name === ERROR_MAP[500]) {
175+
return t('There was an internal systems error.');
176+
}
177+
return t(
178+
'This could be due to invalid search parameters or an internal systems error.'
179+
);
180+
}
181+
182+
const RowContentButton = styled('button')`
183+
display: contents;
184+
cursor: pointer;
185+
186+
border: none;
187+
background: transparent;
188+
margin: 0;
189+
padding: 0;
190+
`;
191+
192+
const RowCell = styled(SimpleTable.RowCell)`
193+
position: relative;
194+
overflow: auto;
195+
196+
&:hover [data-visible-on-hover='true'] {
197+
opacity: 1;
198+
}
199+
`;

static/app/components/replays/table/replayTableColumns.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ interface RenderProps {
5454
showDropdownFilters: boolean;
5555
}
5656

57-
interface ReplayTableColumn {
57+
export interface ReplayTableColumn {
5858
Component: (props: RenderProps) => ReactNode;
5959
name: string;
6060
sortKey: undefined | ReplayRecordNestedFieldName;
@@ -183,7 +183,10 @@ export const ReplayCountErrorsColumn: ReplayTableColumn = {
183183
return null;
184184
}
185185
return (
186-
<DropdownContainer key="countErrors">
186+
<DropdownContainer
187+
key="countErrors"
188+
data-test-id="replay-table-column-count-errors"
189+
>
187190
<TabularNumber>
188191
{replay.count_errors ? (
189192
<Flex gap={space(0.5)}>
@@ -401,7 +404,7 @@ export const ReplaySessionColumn: ReplayTableColumn = {
401404
<Flex key="session" align="center" gap={space(1)}>
402405
<UserAvatar user={getUserBadgeUser(replay)} size={24} />
403406
<SubText>
404-
<Flex gap={space(0.5)}>
407+
<Flex gap={space(0.5)} align="flex-start">
405408
<DisplayNameLink
406409
to={
407410
isIssuesReplayList
@@ -484,7 +487,6 @@ function getUserBadgeUser(replay: ListRecord) {
484487
}
485488

486489
const DropdownContainer = styled(Flex)`
487-
position: relative;
488490
flex-direction: column;
489491
justify-content: center;
490492
`;
@@ -505,6 +507,7 @@ const SubText = styled('div')`
505507
display: flex;
506508
flex-direction: column;
507509
gap: ${space(0.25)};
510+
align-items: flex-start;
508511
`;
509512

510513
const DisplayNameLink = styled(Link)`
@@ -523,7 +526,7 @@ const PlayPauseButtonContainer = styled(Flex)`
523526
flex-direction: column;
524527
justify-content: center;
525528
526-
margin-inline: -${space(1)};
529+
margin: 0 -${space(2)} 0 -${space(1)};
527530
`;
528531

529532
const SpanOperationBreakdown = styled('div')`
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {useCallback, useRef} from 'react';
2+
3+
import {trackAnalytics} from 'sentry/utils/analytics';
4+
import {encodeSort} from 'sentry/utils/discover/eventView';
5+
import type {Sort} from 'sentry/utils/discover/fields';
6+
import {decodeSorts} from 'sentry/utils/queryString';
7+
import useOrganization from 'sentry/utils/useOrganization';
8+
import useUrlParams from 'sentry/utils/useUrlParams';
9+
10+
interface Props {
11+
defaultSort?: Sort;
12+
queryParamKey?: string;
13+
}
14+
15+
const DEFAULT_SORT = {field: 'started_at', kind: 'asc'} as const;
16+
17+
export default function useReplayTableSort({
18+
defaultSort = DEFAULT_SORT,
19+
queryParamKey = 'sort',
20+
}: Props = {}) {
21+
const defaultSortRef = useRef(defaultSort);
22+
const organization = useOrganization();
23+
24+
const {getParamValue, setParamValue} = useUrlParams(queryParamKey, '-started_at');
25+
const sortQuery = getParamValue();
26+
const sortType = decodeSorts(sortQuery).at(0) ?? defaultSortRef.current;
27+
28+
const handleSortClick = useCallback(
29+
(key: string) => {
30+
const newSort = {
31+
field: key,
32+
kind:
33+
key === sortType.field ? (sortType.kind === 'asc' ? 'desc' : 'asc') : 'desc',
34+
} satisfies Sort;
35+
36+
setParamValue(encodeSort(newSort));
37+
38+
trackAnalytics('replay.list-sorted', {
39+
organization,
40+
column: key,
41+
});
42+
},
43+
[organization, setParamValue, sortType]
44+
);
45+
46+
return {
47+
sortType,
48+
sortQuery,
49+
onSortClick: handleSortClick,
50+
};
51+
}

static/app/utils/replays/hooks/useFetchReplayList.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,7 @@ describe('GroupReplays', () => {
246246
deprecatedRouterMocks: true,
247247
});
248248

249-
expect(
250-
await screen.findByText('There are no items to display')
251-
).toBeInTheDocument();
249+
expect(await screen.findByText('No replays found')).toBeInTheDocument();
252250
expect(mockReplayCountApi).toHaveBeenCalled();
253251
expect(mockReplayApi).toHaveBeenCalledTimes(1);
254252
});
@@ -447,14 +445,14 @@ describe('GroupReplays', () => {
447445
expect(screen.getByText('06:40')).toBeInTheDocument();
448446

449447
// Expect the first row to have the correct errors
450-
expect(screen.getAllByTestId('replay-table-count-errors')[0]).toHaveTextContent(
451-
'1'
452-
);
448+
expect(
449+
screen.getAllByTestId('replay-table-column-count-errors')[0]
450+
).toHaveTextContent('1');
453451

454452
// Expect the second row to have the correct errors
455-
expect(screen.getAllByTestId('replay-table-count-errors')[1]).toHaveTextContent(
456-
'4'
457-
);
453+
expect(
454+
screen.getAllByTestId('replay-table-column-count-errors')[1]
455+
).toHaveTextContent('4');
458456

459457
// Expect the first row to have the correct date
460458
expect(screen.getByText('14 days ago')).toBeInTheDocument();

0 commit comments

Comments
 (0)