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 [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()); }, }); const tagSnapshots = useMutation({ ...tagSnapshotsMutation(), onMutate: () => { setShowReTagDialog(false); }, onSuccess: () => { setShowReTagDialog(false); setSelectedIds(new Set()); setTargetScheduleId(""); }, }); const handleRowClick = (snapshotId: string) => { void navigate({ to: `/repositories/${repositoryId}/${snapshotId}` }); }; const toggleSelectAll = () => { if (selectedIds.size === snapshots.length) { setSelectedIds(new Set()); } else { setSelectedIds(new Set(snapshots.map((s) => s.short_id))); } }; 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()}> { const newSelected = new Set(selectedIds); if (newSelected.has(snapshot.short_id)) { newSelected.delete(snapshot.short_id); } else { newSelected.add(snapshot.short_id); } setSelectedIds(newSelected); }} 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.
); };