Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 71 additions & 16 deletions app/client/components/snapshots-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
const { formatDateTime } = useTimeFormat();

const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [lastSelectedId, setLastSelectedId] = useState<string | null>(null);
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
const [showReTagDialog, setShowReTagDialog] = useState(false);
const [targetScheduleId, setTargetScheduleId] = useState<string>("");
Expand All @@ -70,6 +71,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
void queryClient.invalidateQueries({ queryKey });
setShowBulkDeleteConfirm(false);
setSelectedIds(new Set());
setLastSelectedId(null);
},
});

Expand All @@ -81,6 +83,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
onSuccess: () => {
setShowReTagDialog(false);
setSelectedIds(new Set());
setLastSelectedId(null);
setTargetScheduleId("");
},
});
Expand All @@ -92,11 +95,63 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
const toggleSelectAll = () => {
if (selectedIds.size === snapshots.length) {
setSelectedIds(new Set());
setLastSelectedId(null);
} else {
setSelectedIds(new Set(snapshots.map((s) => s.short_id)));
setLastSelectedId(snapshots.length > 0 ? snapshots[snapshots.length - 1].short_id : null);
}
};

const handleSnapshotSelection = (snapshotId: string, event?: React.MouseEvent | React.KeyboardEvent) => {
const isShiftClick = event && "shiftKey" in event && event.shiftKey;

// Attempt range selection first
if (isShiftClick && snapshots.length > 0) {
const currentIndex = snapshots.findIndex((s) => s.short_id === snapshotId);

if (currentIndex !== -1) {
// If lastSelectedId exists, use it; otherwise start from the first item (index 0)
let startIndex: number;
if (lastSelectedId) {
startIndex = snapshots.findIndex((s) => s.short_id === lastSelectedId);
// If lastSelectedId no longer exists in snapshots (stale reference), fall back to single selection
if (startIndex === -1) {
const newSelected = new Set(selectedIds);
if (newSelected.has(snapshotId)) {
newSelected.delete(snapshotId);
} else {
newSelected.add(snapshotId);
}
setSelectedIds(newSelected);
setLastSelectedId(snapshotId);
return;
}
} else {
startIndex = 0;
}

// Valid range selection - replace the entire selection with the new range
const start = Math.min(startIndex, currentIndex);
const end = Math.max(startIndex, currentIndex);
const rangeIds = new Set(snapshots.slice(start, end + 1).map((s) => s.short_id));

setSelectedIds(rangeIds);
setLastSelectedId(snapshots[startIndex].short_id);
return;
}
}

// Single selection toggle (used as fallback or when shift-click not applicable)
const newSelected = new Set(selectedIds);
if (newSelected.has(snapshotId)) {
newSelected.delete(snapshotId);
} else {
newSelected.add(snapshotId);
}
setSelectedIds(newSelected);
setLastSelectedId(snapshotId);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const handleBulkDelete = () => {
toast.promise(
deleteSnapshots.mutateAsync({
Expand Down Expand Up @@ -162,17 +217,12 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
className={cn("hover:bg-accent/50 cursor-pointer", isSelected && "bg-accent/30")}
onClick={() => handleRowClick(snapshot.short_id)}
>
<TableCell onClick={(e) => e.stopPropagation()}>
<TableCell onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={() => {
const newSelected = new Set(selectedIds);
if (newSelected.has(snapshot.short_id)) {
newSelected.delete(snapshot.short_id);
} else {
newSelected.add(snapshot.short_id);
}
setSelectedIds(newSelected);
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
handleSnapshotSelection(snapshot.short_id, e);
}}
aria-label={`Select snapshot ${snapshot.short_id}` as string}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand All @@ -189,7 +239,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
hidden={!backup}
to={backup ? `/backups/$backupId` : "."}
params={backup ? { backupId: backup.shortId } : {}}
onClick={(e) => e.stopPropagation()}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
className="hover:underline"
>
<span className="text-sm">{backup ? backup.name : "-"}</span>
Expand All @@ -216,7 +266,9 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
<TableCell className="hidden md:table-cell">
<div className="flex items-center justify-end gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{formatDuration(snapshot.duration / 1000)}</span>
<span className="text-sm text-muted-foreground">
{formatDuration(snapshot.duration / 1000)}
</span>
</div>
</TableCell>
</TableRow>
Expand All @@ -234,7 +286,10 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => setSelectedIds(new Set())}
onClick={() => {
setSelectedIds(new Set());
setLastSelectedId(null);
}}
>
<X className="h-4 w-4" />
</Button>
Expand Down Expand Up @@ -269,8 +324,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
<AlertDialogHeader>
<AlertDialogTitle>Delete {selectedIds.size} snapshots?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the selected snapshots and all their data from
the repository.
This action cannot be undone. This will permanently delete the selected snapshots and all
their data from the repository.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
Expand All @@ -291,8 +346,8 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshots
<DialogHeader>
<DialogTitle>Re-tag snapshots</DialogTitle>
<DialogDescription>
Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All {selectedIds.size}{" "}
selected snapshots will be associated with the chosen schedule.
Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All&nbsp;
{selectedIds.size} selected snapshots will be associated with the chosen schedule.
</DialogDescription>
</DialogHeader>
<div className="py-4">
Expand Down