refactor: optimistic ui when deleting a snapshot (#515)
This commit is contained in:
parent
bad944a232
commit
8e5eb68935
4 changed files with 56 additions and 12 deletions
|
|
@ -27,19 +27,26 @@ import {
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import { formatDuration } from "~/utils/utils";
|
import { formatDuration } from "~/utils/utils";
|
||||||
import { formatDateTime } from "~/client/lib/datetime";
|
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 { parseError } from "~/client/lib/errors";
|
||||||
import type { BackupSchedule, Snapshot } from "../lib/types";
|
import type { BackupSchedule, Snapshot } from "../lib/types";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { Link, useNavigate } from "@tanstack/react-router";
|
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 = {
|
type Props = {
|
||||||
snapshots: Snapshot[];
|
snapshots: Snapshot[];
|
||||||
backups: BackupSchedule[];
|
backups: BackupSchedule[];
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
|
listSnapshotsQueryOptions: Options<ListSnapshotsData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshotsQueryOptions }: Props) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
@ -50,8 +57,16 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
|
|
||||||
const deleteSnapshots = useMutation({
|
const deleteSnapshots = useMutation({
|
||||||
...deleteSnapshotsMutation(),
|
...deleteSnapshotsMutation(),
|
||||||
onSuccess: () => {
|
onSuccess: (_data, variables) => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
|
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);
|
setShowBulkDeleteConfirm(false);
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
},
|
},
|
||||||
|
|
@ -63,7 +78,6 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
||||||
setShowReTagDialog(false);
|
setShowReTagDialog(false);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
|
|
||||||
setShowReTagDialog(false);
|
setShowReTagDialog(false);
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
setTargetScheduleId("");
|
setTargetScheduleId("");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useId, useState } from "react";
|
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 { toast } from "sonner";
|
||||||
import { Save, X } from "lucide-react";
|
import { Save, X } from "lucide-react";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
|
@ -18,6 +19,7 @@ import {
|
||||||
runBackupNowMutation,
|
runBackupNowMutation,
|
||||||
deleteBackupScheduleMutation,
|
deleteBackupScheduleMutation,
|
||||||
listSnapshotsOptions,
|
listSnapshotsOptions,
|
||||||
|
listSnapshotsQueryKey,
|
||||||
updateBackupScheduleMutation,
|
updateBackupScheduleMutation,
|
||||||
stopBackupMutation,
|
stopBackupMutation,
|
||||||
deleteSnapshotMutation,
|
deleteSnapshotMutation,
|
||||||
|
|
@ -38,6 +40,7 @@ import type {
|
||||||
Repository,
|
Repository,
|
||||||
ScheduleMirror,
|
ScheduleMirror,
|
||||||
ScheduleNotification,
|
ScheduleNotification,
|
||||||
|
Snapshot,
|
||||||
} from "~/client/lib/types";
|
} from "~/client/lib/types";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
|
@ -50,15 +53,18 @@ type Props = {
|
||||||
mirrors: ScheduleMirror[];
|
mirrors: ScheduleMirror[];
|
||||||
};
|
};
|
||||||
scheduleId: string;
|
scheduleId: string;
|
||||||
|
initialSnapshotId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ScheduleDetailsPage(props: Props) {
|
export function ScheduleDetailsPage(props: Props) {
|
||||||
const { loaderData, scheduleId } = props;
|
const { loaderData, scheduleId, initialSnapshotId } = props;
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const searchParams = useSearch({ from: "/(dashboard)/backups/$backupId/" });
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const formId = useId();
|
const formId = useId();
|
||||||
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string>();
|
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | undefined>(initialSnapshotId);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
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({
|
const deleteSnapshot = useMutation({
|
||||||
...deleteSnapshotMutation(),
|
...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);
|
setShowDeleteConfirm(false);
|
||||||
setSnapshotToDelete(null);
|
setSnapshotToDelete(null);
|
||||||
if (selectedSnapshotId === snapshotToDelete) {
|
if (selectedSnapshotId === snapshotId) {
|
||||||
setSelectedSnapshotId(undefined);
|
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) {
|
if (isEditMode) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -250,7 +276,7 @@ export function ScheduleDetailsPage(props: Props) {
|
||||||
snapshots={snapshots ?? []}
|
snapshots={snapshots ?? []}
|
||||||
snapshotId={selectedSnapshot?.short_id}
|
snapshotId={selectedSnapshot?.short_id}
|
||||||
error={failureReason?.message}
|
error={failureReason?.message}
|
||||||
onSnapshotSelect={setSelectedSnapshotId}
|
onSnapshotSelect={handleSnapshotSelect}
|
||||||
/>
|
/>
|
||||||
<BackupSummaryCard summary={selectedSnapshot?.summary} />
|
<BackupSummaryCard summary={selectedSnapshot?.summary} />
|
||||||
{selectedSnapshot && (
|
{selectedSnapshot && (
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
||||||
snapshots={filteredSnapshots}
|
snapshots={filteredSnapshots}
|
||||||
repositoryId={repository.shortId}
|
repositoryId={repository.shortId}
|
||||||
backups={schedules.data ?? []}
|
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">
|
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-between border-t">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { type } from "arktype";
|
||||||
import {
|
import {
|
||||||
getBackupScheduleOptions,
|
getBackupScheduleOptions,
|
||||||
getScheduleMirrorsOptions,
|
getScheduleMirrorsOptions,
|
||||||
|
|
@ -11,6 +12,7 @@ import { ScheduleDetailsPage } from "~/client/modules/backups/routes/backup-deta
|
||||||
|
|
||||||
export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({
|
export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
validateSearch: type({ snapshot: "string?" }),
|
||||||
loader: async ({ params, context }) => {
|
loader: async ({ params, context }) => {
|
||||||
const { backupId } = params;
|
const { backupId } = params;
|
||||||
|
|
||||||
|
|
@ -48,6 +50,7 @@ export const Route = createFileRoute("/(dashboard)/backups/$backupId/")({
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const loaderData = Route.useLoaderData();
|
const loaderData = Route.useLoaderData();
|
||||||
const { backupId } = Route.useParams();
|
const { backupId } = Route.useParams();
|
||||||
|
const search = Route.useSearch();
|
||||||
|
|
||||||
return <ScheduleDetailsPage loaderData={loaderData} scheduleId={backupId} />;
|
return <ScheduleDetailsPage loaderData={loaderData} scheduleId={backupId} initialSnapshotId={search.snapshot} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue