mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
* chore: remove dead comment in database page * fix(data-table): manualPagination support * fix(hooks): useServerDataTable for server-side pagination * fix(database): paginated backup and restoration list actions * fix(database): server-paginate backup list * fix(database): server-paginate restoration list * fix(database): switch tabs via History API instead of router.push * fix(logs): load job logs on demand when the modal opens * perf(database): slim metadata poll and drop eager backup/log loading * fix(database): invalidate backups/restorations list keys on mutations for instant refresh * fix(logs): disable log button when row has no logs * fix(logs): fetch logs on button click, open modal already populated * fix: loading log modal --------- Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
208 lines
7.0 KiB
TypeScript
208 lines
7.0 KiB
TypeScript
"use client";
|
|
|
|
import { ColumnDef } from "@tanstack/react-table";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Check, MoreHorizontal, Trash2, X } from "lucide-react";
|
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
|
import { StatusBadge } from "@/components/common/status-badge";
|
|
import {Restoration, RestorationWith} from "@/db/schema/07_database";
|
|
import { formatLocalizedDate } from "@/utils/date-formatting";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
deleteRestoreAction,
|
|
rerunRestorationAction,
|
|
} from "@/features/database/actions/restore.action";
|
|
import { toast } from "sonner";
|
|
import { TooltipCustom } from "@/components/common/tooltip-custom";
|
|
import { MemberWithUser } from "@/db/schema/03_organization";
|
|
import { ButtonWithConfirm } from "@/components/common/button-with-confirm";
|
|
import {LogsModalTrigger} from "@/features/logs/components/logs-modal-trigger";
|
|
import {formatDuration} from "@/utils/text";
|
|
|
|
export function restoreColumns(
|
|
isAlreadyRestore: boolean,
|
|
activeMember: MemberWithUser,
|
|
): ColumnDef<RestorationWith>[] {
|
|
return [
|
|
{
|
|
accessorKey: "id",
|
|
header: "Reference",
|
|
},
|
|
{
|
|
accessorKey: "createdAt",
|
|
header: "Created At",
|
|
cell: ({ row }) => {
|
|
return formatLocalizedDate(row.getValue("createdAt"));
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "status",
|
|
header: "Status",
|
|
cell: ({ row }) => {
|
|
return <StatusBadge status={row.getValue("status")} />;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "durationMs",
|
|
header: "Duration",
|
|
cell: ({row}) => {
|
|
const durationMs = row.getValue("durationMs");
|
|
return durationMs ? formatDuration(row.getValue("durationMs")) : "-"
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "logs",
|
|
header: "Logs",
|
|
cell: ({row}) => {
|
|
return <LogsModalTrigger restorationId={row.original.id} hasLogs={row.original.hasLogs}/>;
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => {
|
|
const status = row.getValue("status");
|
|
|
|
const queryClient = useQueryClient();
|
|
const rowData: Restoration = row.original;
|
|
|
|
const mutationDeleteRestore = useMutation({
|
|
mutationFn: async () => {
|
|
const restoration = await deleteRestoreAction({
|
|
restorationId: rowData.id,
|
|
});
|
|
// @ts-ignore
|
|
if (restoration.data.success) {
|
|
// @ts-ignore
|
|
toast.success(restoration.data.actionSuccess.message);
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["database-data", rowData.databaseId],
|
|
});
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["restorations", rowData.databaseId],
|
|
});
|
|
} else {
|
|
// @ts-ignore
|
|
toast.error(restoration.data.actionError.message);
|
|
}
|
|
},
|
|
});
|
|
|
|
const mutationRerunRestore = useMutation({
|
|
mutationFn: async () => {
|
|
const restoration = await rerunRestorationAction({
|
|
restorationId: rowData.id,
|
|
});
|
|
// @ts-ignore
|
|
if (restoration.data.success) {
|
|
// @ts-ignore
|
|
toast.success(restoration.data.actionSuccess.message);
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["database-data", rowData.databaseId],
|
|
});
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["restorations", rowData.databaseId],
|
|
});
|
|
} else {
|
|
// @ts-ignore
|
|
toast.error(restoration.data.actionError.message);
|
|
}
|
|
},
|
|
});
|
|
|
|
const handleDelete = async () => {
|
|
await mutationDeleteRestore.mutateAsync();
|
|
};
|
|
|
|
const handleRerunRestore = async () => {
|
|
await mutationRerunRestore.mutateAsync();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{activeMember.role !== "member" && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="h-8 w-8 p-0"
|
|
type="button"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<span className="sr-only">Open menu</span>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
{rowData.backupStorageId && (
|
|
<TooltipCustom
|
|
disabled={isAlreadyRestore}
|
|
text="Already a restoration waiting"
|
|
>
|
|
<DropdownMenuItem
|
|
disabled={
|
|
mutationRerunRestore.isPending || isAlreadyRestore
|
|
}
|
|
onClick={async () => {
|
|
await handleRerunRestore();
|
|
}}
|
|
>
|
|
<ReloadIcon /> Rerun
|
|
</DropdownMenuItem>
|
|
</TooltipCustom>
|
|
)}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem>
|
|
<ButtonWithConfirm
|
|
title="Delete Restoration"
|
|
description={
|
|
"Are you sure you want to delete this restoration?"
|
|
}
|
|
button={{
|
|
main: {
|
|
text: "Delete",
|
|
variant: "ghost",
|
|
icon: (
|
|
<Trash2 className="text-red-500 size-4 mr-px" />
|
|
),
|
|
disabled: status === "waiting",
|
|
className:
|
|
"text-red-500 hover:text-red-500 p-0 h-auto has-[>svg]:px-0",
|
|
},
|
|
confirm: {
|
|
className: "w-full",
|
|
text: "Yes, delete",
|
|
icon: <Check />,
|
|
variant: "destructive",
|
|
onClick: async () => {
|
|
await handleDelete();
|
|
},
|
|
},
|
|
cancel: {
|
|
className: "w-full",
|
|
text: "No, cancel",
|
|
icon: <X />,
|
|
variant: "outline",
|
|
},
|
|
}}
|
|
isPending={mutationDeleteRestore.isPending}
|
|
/>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
}
|