From 8e5eb689359a561742a49607bbd7fd5e0e927694 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:47:09 +0100 Subject: [PATCH] refactor: optimistic ui when deleting a snapshot (#515) --- app/client/components/snapshots-table.tsx | 24 +++++++++--- .../modules/backups/routes/backup-details.tsx | 38 ++++++++++++++++--- .../modules/repositories/tabs/snapshots.tsx | 1 + .../(dashboard)/backups/$backupId/index.tsx | 5 ++- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index bcd96847..9b781b8e 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -27,19 +27,26 @@ import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { formatDuration } from "~/utils/utils"; import { formatDateTime } from "~/client/lib/datetime"; -import { deleteSnapshotsMutation, tagSnapshotsMutation } from "~/client/api-client/@tanstack/react-query.gen"; +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 }: Props) => { +export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshotsQueryOptions }: Props) => { const queryClient = useQueryClient(); const navigate = useNavigate(); @@ -50,8 +57,16 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => { const deleteSnapshots = useMutation({ ...deleteSnapshotsMutation(), - onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ["listSnapshots"] }); + 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()); }, @@ -63,7 +78,6 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => { setShowReTagDialog(false); }, onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ["listSnapshots"] }); setShowReTagDialog(false); setSelectedIds(new Set()); setTargetScheduleId(""); diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index 9e31ad3a..3ae6106a 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -1,5 +1,6 @@ import { useId, useState } from "react"; -import { useQuery, useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { useQuery, useMutation, useSuspenseQuery, useQueryClient } from "@tanstack/react-query"; +import { useSearch } from "@tanstack/react-router"; import { toast } from "sonner"; import { Save, X } from "lucide-react"; import { Button } from "~/client/components/ui/button"; @@ -18,6 +19,7 @@ import { runBackupNowMutation, deleteBackupScheduleMutation, listSnapshotsOptions, + listSnapshotsQueryKey, updateBackupScheduleMutation, stopBackupMutation, deleteSnapshotMutation, @@ -38,6 +40,7 @@ import type { Repository, ScheduleMirror, ScheduleNotification, + Snapshot, } from "~/client/lib/types"; import { useNavigate } from "@tanstack/react-router"; @@ -50,15 +53,18 @@ type Props = { mirrors: ScheduleMirror[]; }; scheduleId: string; + initialSnapshotId?: string; }; export function ScheduleDetailsPage(props: Props) { - const { loaderData, scheduleId } = props; + const { loaderData, scheduleId, initialSnapshotId } = props; + const queryClient = useQueryClient(); const navigate = useNavigate(); + const searchParams = useSearch({ from: "/(dashboard)/backups/$backupId/" }); const [isEditMode, setIsEditMode] = useState(false); const formId = useId(); - const [selectedSnapshotId, setSelectedSnapshotId] = useState(); + const [selectedSnapshotId, setSelectedSnapshotId] = useState(initialSnapshotId); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [snapshotToDelete, setSnapshotToDelete] = useState(null); @@ -118,13 +124,25 @@ export function ScheduleDetailsPage(props: Props) { }, }); + const listSnapshotsQueryOptions = { path: { id: schedule.repository.id }, query: { backupId: schedule.shortId } }; + const deleteSnapshot = useMutation({ ...deleteSnapshotMutation(), - onSuccess: () => { + onSuccess: (_data, variables) => { + const snapshotId = variables.path.snapshotId; + const queryKey = listSnapshotsQueryKey(listSnapshotsQueryOptions); + + queryClient.setQueryData(queryKey, (old) => { + if (!old) return old; + return old.filter((snapshot) => snapshot.short_id !== snapshotId); + }); + + void queryClient.invalidateQueries({ queryKey }); setShowDeleteConfirm(false); setSnapshotToDelete(null); - if (selectedSnapshotId === snapshotToDelete) { + if (selectedSnapshotId === snapshotId) { setSelectedSnapshotId(undefined); + void navigate({ to: ".", search: () => ({ snapshot: undefined }) }); } }, }); @@ -200,6 +218,14 @@ export function ScheduleDetailsPage(props: Props) { } }; + const handleSnapshotSelect = (snapshotId: string) => { + setSelectedSnapshotId(snapshotId); + void navigate({ + to: ".", + search: () => ({ ...searchParams, snapshot: snapshotId }), + }); + }; + if (isEditMode) { return (
@@ -250,7 +276,7 @@ export function ScheduleDetailsPage(props: Props) { snapshots={snapshots ?? []} snapshotId={selectedSnapshot?.short_id} error={failureReason?.message} - onSnapshotSelect={setSelectedSnapshotId} + onSnapshotSelect={handleSnapshotSelect} /> {selectedSnapshot && ( diff --git a/app/client/modules/repositories/tabs/snapshots.tsx b/app/client/modules/repositories/tabs/snapshots.tsx index c7b9e211..0215d667 100644 --- a/app/client/modules/repositories/tabs/snapshots.tsx +++ b/app/client/modules/repositories/tabs/snapshots.tsx @@ -173,6 +173,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => { snapshots={filteredSnapshots} repositoryId={repository.shortId} backups={schedules.data ?? []} + listSnapshotsQueryOptions={{ path: { id: repository.id } }} /> )}
diff --git a/app/routes/(dashboard)/backups/$backupId/index.tsx b/app/routes/(dashboard)/backups/$backupId/index.tsx index 3e74f9a9..20a5b610 100644 --- a/app/routes/(dashboard)/backups/$backupId/index.tsx +++ b/app/routes/(dashboard)/backups/$backupId/index.tsx @@ -1,4 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; +import { type } from "arktype"; import { getBackupScheduleOptions, getScheduleMirrorsOptions, @@ -11,6 +12,7 @@ import { ScheduleDetailsPage } from "~/client/modules/backups/routes/backup-deta export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({ component: RouteComponent, + validateSearch: type({ snapshot: "string?" }), loader: async ({ params, context }) => { const { backupId } = params; @@ -48,6 +50,7 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({ function RouteComponent() { const loaderData = Route.useLoaderData(); const { backupId } = Route.useParams(); + const search = Route.useSearch(); - return ; + return ; }