Skip to content

Commit 3110d57

Browse files
authored
feat(replay): Allow the bulk-delete confirm dialog to scroll (#95835)
The table scrolls now when there are many items. When the browser is really short then we'll show as few as ~2.5 items in the list thanks to the `min-height: 200px;` <img width="633" height="734" alt="Screenshot 2025-07-17 at 2 12 53 PM" src="https://github.com/user-attachments/assets/5876487c-b591-4288-b8c9-989cf7f0aa86" /> Fixes REPLAY-533
1 parent 6fb2f2e commit 3110d57

File tree

1 file changed

+79
-83
lines changed

1 file changed

+79
-83
lines changed

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

Lines changed: 79 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ export default function DeleteReplays({selectedIds, replays, queryOptions}: Prop
4141
},
4242
});
4343

44-
const hasOneProjectSelected = projectIds.length === 1;
4544
const project = useProjectFromId({
46-
project_id: hasOneProjectSelected ? projectIds[0] : undefined,
45+
project_id: projectIds.length === 1 ? projectIds[0] : undefined,
4746
});
47+
const hasOneProjectSelected = Boolean(project);
4848

4949
const {bulkDelete, hasAccess, queryOptionsToPayload} = useDeleteReplays({
5050
projectSlug: project?.slug ?? '',
@@ -68,24 +68,21 @@ export default function DeleteReplays({selectedIds, replays, queryOptions}: Prop
6868
onClick={() =>
6969
openConfirmModal({
7070
bypass: selectedIds !== 'all' && selectedIds.length === 1,
71-
renderMessage: _props => (
72-
<Fragment>
73-
{selectedIds === 'all' ? (
74-
<ReplayQueryPreview
75-
deletePayload={deletePayload}
76-
project={project!}
77-
/>
78-
) : (
79-
<ErrorBoundary mini>
80-
<ReplayPreviewTable
81-
replays={replays}
82-
selectedIds={selectedIds}
83-
project={project!}
84-
/>
85-
</ErrorBoundary>
86-
)}
87-
</Fragment>
88-
),
71+
renderMessage: _props =>
72+
selectedIds === 'all' ? (
73+
<ReplayQueryPreview deletePayload={deletePayload} project={project!} />
74+
) : (
75+
<ErrorBoundary mini>
76+
<Title project={project!}>
77+
{tn(
78+
'The following %s replay will be deleted',
79+
'The following %s replays will be deleted',
80+
selectedIds.length
81+
)}
82+
</Title>
83+
<ReplayPreviewTable replays={replays} selectedIds={selectedIds} />
84+
</ErrorBoundary>
85+
),
8986
renderConfirmButton: ({defaultOnClick}) => (
9087
<Button onClick={defaultOnClick} priority="danger">
9188
{t('Delete')}
@@ -148,79 +145,68 @@ function ReplayQueryPreview({
148145
}
149146

150147
function ReplayPreviewTable({
151-
project,
152148
replays,
153149
selectedIds,
154150
}: {
155-
project: Project;
156151
replays: ReplayListRecord[];
157152
selectedIds: string[];
158153
}) {
159154
return (
160-
<Fragment>
161-
<Title project={project}>
162-
{tn(
163-
'The following %s replay will be deleted',
164-
'The following %s replays will be deleted',
165-
selectedIds.length
166-
)}
167-
</Title>
168-
<SimpleTableWithTwoColumns>
169-
<SimpleTable.Header>
170-
<SimpleTable.HeaderCell>{t('Replay')}</SimpleTable.HeaderCell>
171-
<SimpleTable.HeaderCell>{t('Duration')}</SimpleTable.HeaderCell>
172-
</SimpleTable.Header>
173-
{selectedIds.map(id => {
174-
const replay = replays.find(r => r.id === id) as ReplayListRecord;
175-
if (replay.is_archived) {
176-
return null;
177-
}
178-
invariant(
179-
replay.duration && replay.started_at,
180-
'For TypeScript: replay.duration and replay.started_at are implied because replay.is_archived is false'
181-
);
155+
<SimpleTableWithTwoColumns>
156+
<SimpleTable.Header>
157+
<SimpleTable.HeaderCell>{t('Replay')}</SimpleTable.HeaderCell>
158+
<SimpleTable.HeaderCell>{t('Duration')}</SimpleTable.HeaderCell>
159+
</SimpleTable.Header>
160+
{selectedIds.map(id => {
161+
const replay = replays.find(r => r.id === id) as ReplayListRecord;
162+
if (replay.is_archived) {
163+
return null;
164+
}
165+
invariant(
166+
replay.duration && replay.started_at,
167+
'For TypeScript: replay.duration and replay.started_at are implied because replay.is_archived is false'
168+
);
182169

183-
return (
184-
<SimpleTable.Row key={id}>
185-
<SimpleTable.RowCell>
186-
<Flex key="session" align="center" gap={space(1)}>
187-
<UserAvatar
188-
user={{
189-
username: replay.user?.display_name || '',
190-
email: replay.user?.email || '',
191-
id: replay.user?.id || '',
192-
ip_address: replay.user?.ip || '',
193-
name: replay.user?.username || '',
194-
}}
195-
size={24}
196-
/>
197-
<SubText>
198-
<Flex gap={space(0.5)} align="flex-start">
199-
<DisplayName>
200-
{replay.user.display_name || t('Anonymous User')}
201-
</DisplayName>
202-
</Flex>
170+
return (
171+
<SimpleTable.Row key={id}>
172+
<SimpleTable.RowCell>
173+
<Flex key="session" align="center" gap={space(1)}>
174+
<UserAvatar
175+
user={{
176+
username: replay.user?.display_name || '',
177+
email: replay.user?.email || '',
178+
id: replay.user?.id || '',
179+
ip_address: replay.user?.ip || '',
180+
name: replay.user?.username || '',
181+
}}
182+
size={24}
183+
/>
184+
<SubText>
185+
<Flex gap={space(0.5)} align="flex-start">
186+
<DisplayName>
187+
{replay.user.display_name || t('Anonymous User')}
188+
</DisplayName>
189+
</Flex>
190+
<Flex gap={space(0.5)}>
191+
{getShortEventId(replay.id)}
203192
<Flex gap={space(0.5)}>
204-
{getShortEventId(replay.id)}
205-
<Flex gap={space(0.5)}>
206-
<IconCalendar color="gray300" size="xs" />
207-
<TimeSince date={replay.started_at} />
208-
</Flex>
193+
<IconCalendar color="gray300" size="xs" />
194+
<TimeSince date={replay.started_at} />
209195
</Flex>
210-
</SubText>
211-
</Flex>
212-
</SimpleTable.RowCell>
213-
<SimpleTable.RowCell justify="flex-end">
214-
<Duration
215-
duration={[replay.duration.asMilliseconds() ?? 0, 'ms']}
216-
precision="sec"
217-
/>
218-
</SimpleTable.RowCell>
219-
</SimpleTable.Row>
220-
);
221-
})}
222-
</SimpleTableWithTwoColumns>
223-
</Fragment>
196+
</Flex>
197+
</SubText>
198+
</Flex>
199+
</SimpleTable.RowCell>
200+
<SimpleTable.RowCell justify="flex-end">
201+
<Duration
202+
duration={[replay.duration.asMilliseconds() ?? 0, 'ms']}
203+
precision="sec"
204+
/>
205+
</SimpleTable.RowCell>
206+
</SimpleTable.Row>
207+
);
208+
})}
209+
</SimpleTableWithTwoColumns>
224210
);
225211
}
226212

@@ -246,6 +232,16 @@ function Title({children, project}: {children: React.ReactNode; project: Project
246232

247233
const SimpleTableWithTwoColumns = styled(SimpleTable)`
248234
grid-template-columns: 1fr max-content;
235+
236+
max-height: calc(100vh - 315px);
237+
min-height: 200px;
238+
overflow-y: auto;
239+
240+
& > div:first-child {
241+
position: sticky;
242+
top: 0;
243+
z-index: 1;
244+
}
249245
`;
250246

251247
const SubText = styled('div')`

0 commit comments

Comments
 (0)