Finish working on backups side deletion feature, to be review, test and deply in RC for tests.

This commit is contained in:
charlesgauthereau
2025-09-22 20:31:29 +02:00
parent 34ff11f377
commit 654ff6b9a5
10 changed files with 426 additions and 185 deletions
@@ -148,7 +148,8 @@ export function DataTable<TData, TValue>({
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}
className={highlightRow && highlightRow(row.original) ? "bg-gray-100 pointer-events-none" : ""}
data-state={row.getIsSelected() && "selected"}>
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
@@ -0,0 +1,66 @@
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {Toggle} from "@/components/ui/toggle";
import {CheckIcon, Filter, RefreshCcw} from "lucide-react";
import {Badge} from "@/components/ui/badge";
export type FilterItem = {
label: string,
value: string
}
type FiltersDropdownProps = {
items: FilterItem[];
selectedItems: FilterItem[];
onSelect: (item: FilterItem) => void;
clearFilters: () => void;
}
export const FiltersDropdown = ({items, selectedItems, onSelect, clearFilters}: FiltersDropdownProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="relative">
<Toggle variant="outline" size="sm" className="cursor-pointer w-fit">
<Filter className="h-4 w-4"/>
</Toggle>
{selectedItems.length > 0 && (
<Badge className="absolute -top-2 -right-2 h-4 w-4 rounded-full p-0 flex items-center justify-center text-[10px]">
{selectedItems.length}
</Badge>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
{items.map((item, index) => {
const isSelected = selectedItems.some(f => f.value === item.value);
return (
<DropdownMenuItem
key={index}
className="flex items-center justify-between cursor-pointer"
onClick={(e) => {
e.preventDefault();
onSelect(item);
}}
>
<span>{item.label}</span>
{isSelected && <CheckIcon className="h-4 w-4 text-blue-500"/>}
</DropdownMenuItem>
)
})}
<DropdownMenuItem
disabled={selectedItems.length === 0}
className="flex gap-2 cursor-pointer"
onClick={(e) => {
e.preventDefault();
clearFilters();
}}
>
<RefreshCcw className='h-4 w-4'/>
Clear filters
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,146 @@
"use client"
import {backupColumns} from "@/features/dashboard/backup/columns";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {MoreHorizontal, Trash2} from "lucide-react";
import {FilterItem, FiltersDropdown} from "@/components/wrappers/common/table/filters";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {useMemo, useState} from "react";
import {Backup, DatabaseWith} from "@/db/schema/07_database";
import {Setting} from "@/db/schema/01_setting";
import {useMutation} from "@tanstack/react-query";
import {deleteBackupAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
type DatabaseBackupListProps = {
isAlreadyRestore: boolean;
settings: Setting;
database: DatabaseWith;
backups: Backup[];
}
export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
const items = [
{label: "Deleted", value: "deleted"},
{label: "Available", value: "available"},
]
const [selectedFilters, setSelectedFilters] = useState<FilterItem[]>([items[1]]);
const router = useRouter();
const filteredBackups = useMemo(() => {
if (!props.backups) return [];
return props.backups.filter(backup => {
// --- Status Filter ---
if (selectedFilters.length > 0) {
const selectedValues = selectedFilters.map(f => f.value);
const status = backup.deletedAt != null ? "deleted" : "available";
if (!selectedValues.includes(status)) return false;
}
return true;
});
}, [props.backups, selectedFilters]);
const handleSelectFilter = (item: FilterItem) => {
setSelectedFilters(prev =>
prev.some(f => f.value === item.value)
? prev.filter(f => f.value !== item.value)
: [...prev, item]
);
};
const clearFilters = () => setSelectedFilters([]);
const mutationDeleteBackups = useMutation({
mutationFn: async (backups: Backup[]) => {
const results = await Promise.all(
backups.map(async (backup) => {
if (backup.deletedAt == null) {
const backupDeleted = await deleteBackupAction({
backupId: backup.id,
databaseId: backup.databaseId,
file: backup.file!,
projectSlug: props.database?.project?.slug!
});
return {
success: backupDeleted?.data?.success,
message: backupDeleted?.data?.success
? backupDeleted?.data?.actionSuccess?.message
// @ts-ignore
: restoration?.data?.actionError.message,
};
}
return {
success: true,
message: `Already deleted this backup (ref: ${backup.id}).`,
}
})
);
results.forEach((result) => {
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
});
router.refresh();
},
});
return (
<DataTable
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)}
data={filteredBackups}
enablePagination
selectedActions={(rows) => (
<div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0">
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteBackups.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={async () => {
console.log("Deleting rows:", rows)
await mutationDeleteBackups.mutateAsync(rows)
}}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<FiltersDropdown
items={items}
selectedItems={selectedFilters}
onSelect={handleSelectFilter}
clearFilters={clearFilters}
/>
</div>
</div>
)}
/>
)
}
@@ -0,0 +1,87 @@
"use client"
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {restoreColumns} from "@/features/dashboard/restore/columns";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {MoreHorizontal, Trash2} from "lucide-react";
import {Restoration} from "@/db/schema/07_database";
import {useMutation} from "@tanstack/react-query";
import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
type DatabaseRestoreListProps = {
isAlreadyRestore: boolean;
restorations: Restoration[];
}
export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
const router = useRouter();
const mutationDeleteRestorations = useMutation({
mutationFn: async (restorations: Restoration[]) => {
const results = await Promise.all(
restorations.map(async (restoration) => {
const restorationDeleted = await deleteRestoreAction({
restorationId: restoration.id,
});
return {
success: restorationDeleted?.data?.success,
message: restorationDeleted?.data?.success
? restorationDeleted?.data?.actionSuccess?.message
// @ts-ignore
: restorationDeleted?.data?.actionError.message,
};
})
);
results.forEach((result) => {
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
});
router.refresh();
},
});
return (
<DataTable
columns={restoreColumns(props.isAlreadyRestore)}
data={props.restorations}
enablePagination
selectedActions={(rows) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteRestorations.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={async () => {
await mutationDeleteRestorations.mutateAsync(rows)
}}
disabled={props.isAlreadyRestore}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
/>
)
}
@@ -4,17 +4,10 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {useEffect, useState} from "react";
import {useRouter, useSearchParams} from "next/navigation";
import {eventUpdate} from "@/types/events";
import {backupColumns} from "@/features/dashboard/backup/columns";
import {restoreColumns} from "@/features/dashboard/restore/columns";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_database";
import {Setting} from "@/db/schema/01_setting";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {MoreHorizontal, Trash2} from "lucide-react";
import {useMutation} from "@tanstack/react-query";
import {deleteBackupAction, deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list";
import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list";
export type DatabaseTabsProps = {
settings: Setting
@@ -44,7 +37,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
};
}, []);
useEffect(() => {
const newTab = searchParams.get("tab") ?? "backup";
setTab(newTab);
@@ -54,65 +46,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
router.push(`?tab=${value}`);
};
const mutationDeleteBackups = useMutation({
mutationFn: async (backups: Backup[]) => {
const results = await Promise.all(
backups.map(async (backup) => {
const backupDeleted = await deleteBackupAction({
backupId: backup.id,
databaseId: backup.databaseId,
file: backup.file!,
projectSlug: props.database?.project?.slug!
});
return {
success: backupDeleted?.data?.success,
message: backupDeleted?.data?.success
? backupDeleted?.data?.actionSuccess?.message
// @ts-ignore
: restoration?.data?.actionError.message,
};
})
);
results.forEach((result) => {
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
});
router.refresh();
},
});
const mutationDeleteRestorations = useMutation({
mutationFn: async (restorations: Restoration[]) => {
const results = await Promise.all(
restorations.map(async (restoration) => {
const restorationDeleted = await deleteRestoreAction({
restorationId: restoration.id,
});
return {
success: restorationDeleted?.data?.success,
message: restorationDeleted?.data?.success
? restorationDeleted?.data?.actionSuccess?.message
// @ts-ignore
: restorationDeleted?.data?.actionError.message,
};
})
);
results.forEach((result) => {
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
});
router.refresh();
},
});
return (
<Tabs className="flex flex-col flex-1" value={tab} onValueChange={handleChangeTab}>
<TabsList className="grid w-full grid-cols-2">
@@ -120,83 +53,17 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
<TabsTrigger value="restore">Restoration</TabsTrigger>
</TabsList>
<TabsContent className="h-full justify-between" value="backup">
<DataTable
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)}
data={props.backups}
enablePagination
selectedActions={(rows) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteBackups.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{/*<DropdownMenuItem*/}
{/* onClick={() => {*/}
{/* console.log("Deleting rows:", rows)*/}
{/* }}*/}
{/*>*/}
{/* <Download className="w-4 h-4 mr-2"/>*/}
{/* Download Selected*/}
{/*</DropdownMenuItem>*/}
{/*<Separator/>*/}
<DropdownMenuItem
onClick={async () => {
console.log("Deleting rows:", rows)
await mutationDeleteBackups.mutateAsync(rows)
}}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<DatabaseBackupList
isAlreadyRestore={props.isAlreadyRestore}
settings={props.settings}
database={props.database}
backups={props.backups}
/>
</TabsContent>
<TabsContent className="h-full justify-between" value="restore">
<DataTable
columns={restoreColumns(props.isAlreadyRestore)}
data={props.restorations}
enablePagination
selectedActions={(rows) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteRestorations.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={async () => {
await mutationDeleteRestorations.mutateAsync(rows)
}}
disabled={props.isAlreadyRestore}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<DatabaseRestoreList
isAlreadyRestore={props.isAlreadyRestore}
restorations={props.restorations}
/>
</TabsContent>
</Tabs>