mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: backup delete/restore actions.
This commit is contained in:
@@ -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
|
||||
|
||||
+136
-82
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user