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) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow key={row.id}
|
<TableRow key={row.id}
|
||||||
className={highlightRow && highlightRow(row.original) ? "bg-gray-100 pointer-events-none" : ""}
|
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) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</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 {useEffect, useState} from "react";
|
||||||
import {useRouter, useSearchParams} from "next/navigation";
|
import {useRouter, useSearchParams} from "next/navigation";
|
||||||
import {eventUpdate} from "@/types/events";
|
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 {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_database";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
|
import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list";
|
||||||
import {MoreHorizontal, Trash2} from "lucide-react";
|
import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list";
|
||||||
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";
|
|
||||||
|
|
||||||
export type DatabaseTabsProps = {
|
export type DatabaseTabsProps = {
|
||||||
settings: Setting
|
settings: Setting
|
||||||
@@ -44,7 +37,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newTab = searchParams.get("tab") ?? "backup";
|
const newTab = searchParams.get("tab") ?? "backup";
|
||||||
setTab(newTab);
|
setTab(newTab);
|
||||||
@@ -54,65 +46,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
|||||||
router.push(`?tab=${value}`);
|
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 (
|
return (
|
||||||
<Tabs className="flex flex-col flex-1" value={tab} onValueChange={handleChangeTab}>
|
<Tabs className="flex flex-col flex-1" value={tab} onValueChange={handleChangeTab}>
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
@@ -120,83 +53,17 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
|||||||
<TabsTrigger value="restore">Restoration</TabsTrigger>
|
<TabsTrigger value="restore">Restoration</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent className="h-full justify-between" value="backup">
|
<TabsContent className="h-full justify-between" value="backup">
|
||||||
<DataTable
|
<DatabaseBackupList
|
||||||
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)}
|
isAlreadyRestore={props.isAlreadyRestore}
|
||||||
data={props.backups}
|
settings={props.settings}
|
||||||
enablePagination
|
database={props.database}
|
||||||
selectedActions={(rows) => (
|
backups={props.backups}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent className="h-full justify-between" value="restore">
|
<TabsContent className="h-full justify-between" value="restore">
|
||||||
<DataTable
|
<DatabaseRestoreList
|
||||||
columns={restoreColumns(props.isAlreadyRestore)}
|
isAlreadyRestore={props.isAlreadyRestore}
|
||||||
data={props.restorations}
|
restorations={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>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -28,23 +28,43 @@ import {Setting} from "@/db/schema/01_setting";
|
|||||||
import {SafeActionResult} from "next-safe-action";
|
import {SafeActionResult} from "next-safe-action";
|
||||||
import {ZodString} from "zod";
|
import {ZodString} from "zod";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
|
||||||
export function backupColumns(isAlreadyRestore: boolean, settings: Setting, database: DatabaseWith): ColumnDef<Backup>[] {
|
export function backupColumns(isAlreadyRestore: boolean, settings: Setting, database: DatabaseWith): ColumnDef<Backup>[] {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
id: "availability",
|
||||||
|
cell: ({row}) => {
|
||||||
|
const colorStatus = row.original.deletedAt != null ? "bg-red-400 border-red-600" : "bg-green-400 border-green-600";
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className={cn("w-5 h-5 rounded-full border-4", colorStatus)}/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{row.original.deletedAt != null && (
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{formatFrenchDate(row.getValue("deletedAt"))}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
header: "Reference",
|
header: "Reference",
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
accessorKey: "deletedAt",
|
// accessorKey: "deletedAt",
|
||||||
header: "Deleted At",
|
// header: "Deleted At",
|
||||||
cell: ({row}) => {
|
// cell: ({row}) => {
|
||||||
|
// return row.original.deletedAt ? formatFrenchDate(row.getValue("deletedAt")) : "-"
|
||||||
return row.original.deletedAt ? formatFrenchDate(row.getValue("deletedAt")) : ""
|
// },
|
||||||
},
|
// },
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: "createdAt",
|
accessorKey: "createdAt",
|
||||||
header: "Created At",
|
header: "Created At",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
uploadS3Private
|
uploadS3Private
|
||||||
} from "@/features/upload/private/upload.action";
|
} from "@/features/upload/private/upload.action";
|
||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
export const deleteRestoreAction = userAction
|
export const deleteRestoreAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
@@ -63,6 +64,7 @@ export const deleteBackupAction = userAction
|
|||||||
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
|
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
||||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return {
|
return {
|
||||||
@@ -76,6 +78,12 @@ export const deleteBackupAction = userAction
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(drizzleDb.schemas.backup)
|
||||||
|
.set(withUpdatedAt({
|
||||||
|
deletedAt: new Date(),
|
||||||
|
}))
|
||||||
|
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||||
|
|
||||||
let success: boolean, message: string;
|
let success: boolean, message: string;
|
||||||
|
|
||||||
@@ -98,36 +106,79 @@ export const deleteBackupAction = userAction
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
return {
|
||||||
.delete(drizzleDb.schemas.backup)
|
success: true,
|
||||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
actionSuccess: {
|
||||||
.execute();
|
message: `Backup deleted successfully (ref: ${parsedInput.backupId}).`,
|
||||||
|
},
|
||||||
const backupExists = await db
|
};
|
||||||
.select()
|
|
||||||
.from(drizzleDb.schemas.backup)
|
|
||||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
|
|
||||||
if (backupExists.length === 0) {
|
// const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
return {
|
// if (!settings) {
|
||||||
success: true,
|
// return {
|
||||||
actionSuccess: {
|
// success: false,
|
||||||
message: "Backup deleted successfully.",
|
// actionError: {
|
||||||
},
|
// message: "No settings found.",
|
||||||
};
|
// status: 404,
|
||||||
} else {
|
// cause: "No settings found.",
|
||||||
return {
|
// messageParams: {message: "Error deleting the backup"},
|
||||||
success: false,
|
// },
|
||||||
actionError: {
|
// };
|
||||||
message: "Backup not found or already deleted.",
|
// }
|
||||||
status: 404,
|
//
|
||||||
cause: "Backup could not be deleted (from database or remote storage).",
|
//
|
||||||
messageParams: {message: "Error deleting the backup"},
|
// let success: boolean, message: string;
|
||||||
},
|
//
|
||||||
};
|
// const result =
|
||||||
}
|
// settings.storage === "local"
|
||||||
|
// ? await deleteLocalPrivate(parsedInput.file)
|
||||||
|
// : await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
|
||||||
|
//
|
||||||
|
// ({success, message} = result);
|
||||||
|
//
|
||||||
|
// if (!success) {
|
||||||
|
// return {
|
||||||
|
// success: false,
|
||||||
|
// actionError: {
|
||||||
|
// message: message,
|
||||||
|
// status: 404,
|
||||||
|
// cause: "Unable to delete backup from storage",
|
||||||
|
// messageParams: {message: "Error deleting the backup"},
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// await db
|
||||||
|
// .delete(drizzleDb.schemas.backup)
|
||||||
|
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||||
|
// .execute();
|
||||||
|
//
|
||||||
|
// const backupExists = await db
|
||||||
|
// .select()
|
||||||
|
// .from(drizzleDb.schemas.backup)
|
||||||
|
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||||
|
// .execute();
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// if (backupExists.length === 0) {
|
||||||
|
// return {
|
||||||
|
// success: true,
|
||||||
|
// actionSuccess: {
|
||||||
|
// message: "Backup deleted successfully.",
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// } else {
|
||||||
|
// return {
|
||||||
|
// success: false,
|
||||||
|
// actionError: {
|
||||||
|
// message: "Backup not found or already deleted.",
|
||||||
|
// status: 404,
|
||||||
|
// cause: "Backup could not be deleted (from database or remote storage).",
|
||||||
|
// messageParams: {message: "Error deleting the backup"},
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {enforceRetentionGFS} from "@/lib/tasks/database/retention-gsf";
|
|||||||
import {retentionPolicy} from "@/db/schema/07_database";
|
import {retentionPolicy} from "@/db/schema/07_database";
|
||||||
import {eq, isNull} from "drizzle-orm";
|
import {eq, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {eventEmitter} from "../../../../app/api/events/route";
|
||||||
|
|
||||||
|
|
||||||
export const retentionCleanTask = async () => {
|
export const retentionCleanTask = async () => {
|
||||||
@@ -54,4 +55,5 @@ export async function enforceRetention(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq, lt, and, desc} from "drizzle-orm";
|
import {eq, lt, and, desc, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||||
|
|
||||||
@@ -9,7 +9,8 @@ export async function enforceRetentionDays(databaseId: string, days: number) {
|
|||||||
const expiredBackups = await db.query.backup.findMany({
|
const expiredBackups = await db.query.backup.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||||
lt(drizzleDb.schemas.backup.createdAt, cutoff)
|
lt(drizzleDb.schemas.backup.createdAt, cutoff),
|
||||||
|
isNull(drizzleDb.schemas.backup.deletedAt)
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
database: {
|
database: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {subDays, subWeeks, subMonths, subYears, startOfWeek, startOfMonth, startOfYear} from "date-fns";
|
import {subDays, subWeeks, subMonths, subYears, startOfWeek, startOfMonth, startOfYear} from "date-fns";
|
||||||
import {eq, desc} from "drizzle-orm";
|
import {eq, desc, isNull, and} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
|
|||||||
yearly: number;
|
yearly: number;
|
||||||
}) {
|
}) {
|
||||||
const backups = await db.query.backup.findMany({
|
const backups = await db.query.backup.findMany({
|
||||||
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseId), isNull(drizzleDb.schemas.backup.deletedAt)),
|
||||||
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
orderBy: desc(drizzleDb.schemas.backup.createdAt),
|
||||||
with: {
|
with: {
|
||||||
database: {
|
database: {
|
||||||
|
|||||||
Reference in New Issue
Block a user