mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Finish working on backups side deletion feature, to be review, test and deply in RC for tests.
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user