refactor: optimistic ui when deleting a snapshot (#515)

This commit is contained in:
Nico 2026-02-14 13:47:09 +01:00 committed by GitHub
parent bad944a232
commit 8e5eb68935
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 56 additions and 12 deletions

View file

@ -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<ListSnapshotsData>;
};
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<Snapshot[]>(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("");

View file

@ -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<string>();
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | undefined>(initialSnapshotId);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(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<Snapshot[]>(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 (
<div>
@ -250,7 +276,7 @@ export function ScheduleDetailsPage(props: Props) {
snapshots={snapshots ?? []}
snapshotId={selectedSnapshot?.short_id}
error={failureReason?.message}
onSnapshotSelect={setSelectedSnapshotId}
onSnapshotSelect={handleSnapshotSelect}
/>
<BackupSummaryCard summary={selectedSnapshot?.summary} />
{selectedSnapshot && (

View file

@ -173,6 +173,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
snapshots={filteredSnapshots}
repositoryId={repository.shortId}
backups={schedules.data ?? []}
listSnapshotsQueryOptions={{ path: { id: repository.id } }}
/>
)}
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-between border-t">

View file

@ -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 <ScheduleDetailsPage loaderData={loaderData} scheduleId={backupId} />;
return <ScheduleDetailsPage loaderData={loaderData} scheduleId={backupId} initialSnapshotId={search.snapshot} />;
}