import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Calendar, Clock, Database, HardDrive, Tag, Trash2, X } from "lucide-react"; import { toast } from "sonner"; import { ByteSize } from "~/client/components/bytes-size"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; import { Button } from "~/client/components/ui/button"; import { Checkbox } from "~/client/components/ui/checkbox"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "~/client/components/ui/alert-dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/client/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { useTimeFormat } from "~/client/lib/datetime"; import { formatDuration } from "~/utils/utils"; import { deleteSnapshotsMutation, listSnapshotsQueryKey, tagSnapshotsMutation, } from "~/client/api-client/@tanstack/react-query.gen"; import { parseError } from "~/client/lib/errors"; import type { BackupSchedule, Snapshot } from "../lib/types"; import { cn } from "../lib/utils"; import { Link, useNavigate } from "@tanstack/react-router"; import type { ListSnapshotsData } from "~/client/api-client/types.gen"; import type { Options } from "~/client/api-client/client/types.gen"; type Props = { snapshots: Snapshot[]; backups: BackupSchedule[]; repositoryId: string; listSnapshotsQueryOptions: Options; }; export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshotsQueryOptions }: Props) => { const queryClient = useQueryClient(); const navigate = useNavigate(); const { formatDateTime } = useTimeFormat(); const [selectedIds, setSelectedIds] = useState>(new Set()); const [lastSelectedId, setLastSelectedId] = useState(null); const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false); const [showReTagDialog, setShowReTagDialog] = useState(false); const [targetScheduleId, setTargetScheduleId] = useState(""); const deleteSnapshots = useMutation({ ...deleteSnapshotsMutation(), onSuccess: (_data, variables) => { const snapshotIds = variables.body?.snapshotIds ?? []; const queryKey = listSnapshotsQueryKey(listSnapshotsQueryOptions); queryClient.setQueryData(queryKey, (old) => { if (!old) return old; return old.filter((snapshot) => !snapshotIds.includes(snapshot.short_id)); }); void queryClient.invalidateQueries({ queryKey }); setShowBulkDeleteConfirm(false); setSelectedIds(new Set()); setLastSelectedId(null); }, }); const tagSnapshots = useMutation({ ...tagSnapshotsMutation(), onMutate: () => { setShowReTagDialog(false); }, onSuccess: () => { setShowReTagDialog(false); setSelectedIds(new Set()); setLastSelectedId(null); setTargetScheduleId(""); }, }); const handleRowClick = (snapshotId: string) => { void navigate({ to: `/repositories/${repositoryId}/${snapshotId}` }); }; 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; if (isShiftClick && lastSelectedId) { // Range selection const lastIndex = snapshots.findIndex((s) => s.short_id === lastSelectedId); const currentIndex = snapshots.findIndex((s) => s.short_id === snapshotId); if (lastIndex !== -1 && currentIndex !== -1) { const start = Math.min(lastIndex, currentIndex); const end = Math.max(lastIndex, currentIndex); const rangeIds = new Set(snapshots.slice(start, end + 1).map((s) => s.short_id)); // Add selected range to existing selection const newSelected = new Set(selectedIds); rangeIds.forEach((id) => newSelected.add(id)); setSelectedIds(newSelected); } } else { // Single selection toggle const newSelected = new Set(selectedIds); if (newSelected.has(snapshotId)) { newSelected.delete(snapshotId); } else { newSelected.add(snapshotId); } setSelectedIds(newSelected); } setLastSelectedId(snapshotId); }; const handleBulkDelete = () => { toast.promise( deleteSnapshots.mutateAsync({ path: { shortId: repositoryId }, body: { snapshotIds: Array.from(selectedIds) }, }), { loading: `Deleting ${selectedIds.size} snapshots...`, success: "Snapshots deleted successfully", error: (error) => parseError(error)?.message || "Failed to delete snapshots", }, ); }; const handleBulkReTag = () => { const schedule = backups.find((b) => b.shortId === targetScheduleId); if (!schedule) return; toast.promise( tagSnapshots.mutateAsync({ path: { shortId: repositoryId }, body: { snapshotIds: Array.from(selectedIds), set: [schedule.shortId], }, }), { loading: `Re-tagging ${selectedIds.size} snapshots...`, success: `Snapshots re-tagged to ${schedule.name}`, error: (error) => parseError(error)?.message || "Failed to re-tag snapshots", }, ); }; return ( <>
0} onCheckedChange={toggleSelectAll} aria-label="Select all" /> Snapshot ID Schedule Date & Time Size Duration {snapshots.map((snapshot) => { const backup = backups.find((b) => snapshot.tags.includes(b.shortId)); const isSelected = selectedIds.has(snapshot.short_id); return ( handleRowClick(snapshot.short_id)} > e.stopPropagation()}> { handleSnapshotSelection(snapshot.short_id); }} onClick={(e: React.MouseEvent) => { e.stopPropagation(); if (e.shiftKey) { handleSnapshotSelection(snapshot.short_id, e); } }} aria-label={`Select snapshot ${snapshot.short_id}` as string} />
{snapshot.short_id}
e.stopPropagation()} className="hover:underline" > {backup ? backup.name : "-"}
{formatDateTime(snapshot.time)}
{formatDuration(snapshot.duration / 1000)}
); })}
{selectedIds.size > 0 && (
{selectedIds.size} selected
)} Delete {selectedIds.size} snapshots? This action cannot be undone. This will permanently delete the selected snapshots and all their data from the repository. Cancel Delete {selectedIds.size} snapshots Re-tag snapshots Select a backup schedule to re-tag the {selectedIds.size} selected snapshots. All{" "} {selectedIds.size} selected snapshots will be associated with the chosen schedule.
); };