feat: backup delete/restore actions.

This commit is contained in:
charlesgauthereau
2026-01-17 19:55:17 +01:00
parent 83ef0ee74e
commit 424fa581b9
4 changed files with 294 additions and 270 deletions
@@ -15,19 +15,17 @@ import {MoreHorizontal, Trash2, Download} from "lucide-react";
import {ReloadIcon} from "@radix-ui/react-icons";
import {cn} from "@/lib/utils";
import {MemberWithUser} from "@/db/schema/03_organization";
import {useMutation} from "@tanstack/react-query";
import {deleteBackupAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner";
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
interface DatabaseActionsCellProps {
backup: BackupWith;
activeMember: MemberWithUser;
isAlreadyRestore: boolean;
}
export function DatabaseActionsCell({backup, activeMember}: DatabaseActionsCellProps) {
export function DatabaseActionsCell({backup, activeMember, isAlreadyRestore}: DatabaseActionsCellProps) {
const {openModal} = useBackupModal();
if (backup.deletedAt || activeMember.role === "member") return null;
@@ -42,12 +40,26 @@ export function DatabaseActionsCell({backup, activeMember}: DatabaseActionsCellP
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem onSelect={() => openModal("restore", backup)}>
<ReloadIcon/> Restore
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => openModal("download", backup)}>
<Download/> Download
</DropdownMenuItem>
{backup.status == "success" ? (
<>
<TooltipCustom disabled={isAlreadyRestore}
text="Already a restoration waiting">
<DropdownMenuItem
disabled={isAlreadyRestore}
onSelect={() => openModal("restore", backup)}
>
<ReloadIcon/> Restore
</DropdownMenuItem>
</TooltipCustom>
<DropdownMenuItem
onSelect={() => openModal("download", backup)}
>
<Download/> Download
</DropdownMenuItem>
</>
) : null}
<DropdownMenuSeparator/>
<DropdownMenuItem onSelect={() => openModal("delete", backup)} className="text-red-600">
<Trash2/> Delete
@@ -28,6 +28,9 @@ import {toast} from "sonner";
import {SafeActionResult} from "next-safe-action";
import {ServerActionResult} from "@/types/action-type";
import {ZodString} from "zod";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {AlertCircleIcon, Trash2} from "lucide-react";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
type BackupActionsFormProps = {
backup: BackupWith;
@@ -36,6 +39,7 @@ type BackupActionsFormProps = {
export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
const filteredBackupStorages = backup.storages?.filter((storage) => storage.deletedAt === null) ?? []
const isMobile = useIsMobile();
const {closeModal} = useBackupModal();
@@ -98,8 +102,6 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
const mutationDeleteEntireBackup = useMutation({
mutationFn: async () => {
console.log("mutation deleteEntireBackup");
const result = await deleteBackupAction({
databaseId: backup.databaseId,
backupId: backup.id,
@@ -118,6 +120,8 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
return (
<TooltipProvider>
<Form
form={form}
className="flex flex-col gap-4 mb-1"
@@ -125,102 +129,152 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="backupStorageId"
render={({field}) => (
<FormItem>
<FormLabel>Choose a storage backup</FormLabel>
<FormControl>
<div style={{height: "250px"}}>
<Swiper
direction="vertical"
slidesPerView={3.5}
spaceBetween={10}
// pagination={{ clickable: true }}
mousewheel={{releaseOnEdges: true, forceToAxis: true}}
modules={[Pagination, Mousewheel]}
className="mySwiper"
style={{height: "100%"}}
>
{backup.storages?.filter((storage) => storage.deletedAt === null).map((storage: BackupStorageWith) => (
<SwiperSlide key={storage.id}>
<button
disabled={action !== "delete" && storage.status.toLowerCase() !== "success"}
type="button"
onClick={() => field.onChange(storage.id)}
className={`w-full h-full flex items-start gap-3 p-4 rounded-lg border text-left transition-colors
{filteredBackupStorages.length > 0 ?
<FormField
control={form.control}
name="backupStorageId"
render={({field}) => (
<FormItem>
<FormLabel>Choose a storage backup</FormLabel>
<FormControl>
<div style={{height: "250px"}}>
<Swiper
direction="vertical"
slidesPerView={3.5}
spaceBetween={10}
// pagination={{ clickable: true }}
mousewheel={{releaseOnEdges: true, forceToAxis: true}}
modules={[Pagination, Mousewheel]}
className="mySwiper"
style={{height: "100%"}}
>
{filteredBackupStorages.map((storage: BackupStorageWith) => (
<SwiperSlide key={storage.id}>
<button
disabled={action !== "delete" && storage.status.toLowerCase() !== "success"}
type="button"
onClick={() => field.onChange(storage.id)}
className={`w-full h-full flex items-start gap-3 p-4 rounded-lg border text-left transition-colors
${field.value === storage.id
? "border-foreground bg-background"
: "border-border bg-background" + ((storage.status.toLowerCase() === "success" || action === "delete") ? " hover:border-muted-foreground" : "")}
? "border-foreground bg-background"
: "border-border bg-background" + ((storage.status.toLowerCase() === "success" || action === "delete") ? " hover:border-muted-foreground" : "")}
${storage.status.toLowerCase() !== "success" && action !== "delete" ? "opacity-50 cursor-not-allowed" : ""}`}
>
<div
className={`mt-0.5 h-4 w-4 shrink-0 rounded-full border ${
field.value === storage.id ? "border-foreground" : "border-muted-foreground"
} flex items-center justify-center`}
>
{field.value === storage.id &&
<div className="h-2 w-2 rounded-full bg-foreground"/>}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 flex flex-col gap-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{getChannelIcon(storage.storageChannel?.provider || "")}
<h3 className="font-medium text-foreground">
{isMobile ? truncateWords(storage?.storageChannel?.name ?? "", 2) : storage.storageChannel?.name}
</h3>
<Badge variant="secondary"
className="text-xs font-mono">
{storage.storageChannel?.provider}
<div
className={`mt-0.5 h-4 w-4 shrink-0 rounded-full border ${
field.value === storage.id ? "border-foreground" : "border-muted-foreground"
} flex items-center justify-center`}
>
{field.value === storage.id &&
<div className="h-2 w-2 rounded-full bg-foreground"/>}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 flex flex-col gap-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{getChannelIcon(storage.storageChannel?.provider || "")}
<h3 className="font-medium text-foreground">
{isMobile ? truncateWords(storage?.storageChannel?.name ?? "", 2) : storage.storageChannel?.name}
</h3>
<Badge variant="secondary"
className="text-xs font-mono">
{storage.storageChannel?.provider}
</Badge>
</div>
<Badge variant="outline"
className={`gap-1.5 ${getStatusColor(storage.status)}`}>
{getStatusIcon(storage.status === "success")}
<span
className="capitalize">{storage.status.toUpperCase()}</span>
</Badge>
</div>
<Badge variant="outline"
className={`gap-1.5 ${getStatusColor(storage.status)}`}>
{getStatusIcon(storage.status === "success")}
<span
className="capitalize">{storage.status.toUpperCase()}</span>
</Badge>
</div>
</div>
</div>
</div>
</button>
</SwiperSlide>
)) ?? <p>No storages available</p>}
</Swiper>
</div>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</button>
</SwiperSlide>
)) ?? <p>No storages available</p>}
</Swiper>
</div>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
:
<Alert>
<AlertCircleIcon/>
<AlertTitle>Backup does not have files</AlertTitle>
<AlertDescription>
<p>You can safely delete the entire backup; no files seem to be related. Maybe an error
occurred.</p>
</AlertDescription>
</Alert>
}
<div className="flex flex-row items-center gap-x-4 w-full">
{action === "delete" && (
<ButtonWithLoading
type="button"
variant="destructive"
onClick={() => mutationDeleteEntireBackup.mutateAsync()}
// <ButtonWithLoading
// type="button"
// variant="destructive"
// onClick={() => mutationDeleteEntireBackup.mutateAsync()}
// isPending={mutationDeleteEntireBackup.isPending}
// disabled={mutationDeleteEntireBackup.isPending}
// >
// Delete entire backup
// </ButtonWithLoading>
<ButtonWithConfirm
title={"Delete entire backup"}
description={"Are you sure you want to delete this entire backup?"}
button={{
main: {
type: "button",
variant: "destructive",
text: "Delete entire backup",
},
confirm: {
className: "w-full",
text: "Delete",
icon: <Trash2/>,
variant: "destructive",
onClick: async () => {
mutationDeleteEntireBackup.mutateAsync()
},
},
cancel: {
className: "w-full",
text: "Cancel",
icon: <Trash2/>,
variant: "outline",
},
}}
isPending={mutationDeleteEntireBackup.isPending}
disabled={mutationDeleteEntireBackup.isPending}
/>
)}
{filteredBackupStorages.length > 0 && (
<ButtonWithLoading
type="submit"
isPending={mutation.isPending}
disabled={mutation.isPending}
className="ml-auto"
>
Delete All
Confirm
</ButtonWithLoading>
)}
<ButtonWithLoading
type="submit"
isPending={mutation.isPending}
disabled={mutation.isPending}
className="ml-auto"
>
Confirm
</ButtonWithLoading>
</div>
</Form>
</TooltipProvider>
);
}