update: sensible action button confirm

This commit is contained in:
Théo LAGACHE
2026-03-02 11:21:20 +01:00
parent f1edffed17
commit e644cde514
5 changed files with 500 additions and 418 deletions
@@ -1,89 +1,95 @@
import {PageParams} from "@/types/next"; import { PageParams } from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {
import {currentUser} from "@/lib/auth/current-user"; Page,
import {getActiveMember, getOrganization} from "@/lib/auth/auth"; PageActions,
import {notFound} from "next/navigation"; PageContent,
import {Metadata} from "next"; PageHeader,
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs"; PageTitle,
import {getOrganizationChannels} from "@/db/services/notification-channel"; } from "@/features/layout/page";
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl"; import { currentUser } from "@/lib/auth/current-user";
import {getOrganizationStorageChannels} from "@/db/services/storage-channel"; import { getActiveMember, getOrganization } from "@/lib/auth/auth";
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button"; import { notFound } from "next/navigation";
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog"; import { Metadata } from "next";
import {Button} from "@/components/ui/button"; import { OrganizationTabs } from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
import {GearIcon} from "@radix-ui/react-icons"; import { getOrganizationChannels } from "@/db/services/notification-channel";
import {db} from "@/db"; import { computeOrganizationPermissions } from "@/lib/acl/organization-acl";
import {isNull} from "drizzle-orm"; import { getOrganizationStorageChannels } from "@/db/services/storage-channel";
import { DeleteOrganizationButton } from "@/components/wrappers/dashboard/organization/delete-organization-button";
import { EditOrganizationDialog } from "@/features/organization/components/edit-organization.dialog";
import { db } from "@/db";
import { isNull } from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm"; import { eq } from "drizzle-orm";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Settings", title: "Settings",
}; };
export default async function RoutePage(props: PageParams<{ slug: string }>) { export default async function RoutePage(props: PageParams<{ slug: string }>) {
const organization = await getOrganization({}); const organization = await getOrganization({});
const user = await currentUser(); const user = await currentUser();
const activeMember = await getActiveMember() const activeMember = await getActiveMember();
if (!organization || !activeMember || !user) { if (!organization || !activeMember || !user) {
notFound(); notFound();
} }
const notificationChannels = await getOrganizationChannels(organization.id) const notificationChannels = await getOrganizationChannels(organization.id);
const storageChannels = await getOrganizationStorageChannels(organization.id) const storageChannels = await getOrganizationStorageChannels(organization.id);
const permissions = computeOrganizationPermissions(activeMember); const permissions = computeOrganizationPermissions(activeMember);
const users = await db.query.user.findMany({ const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt) where: (fields) => isNull(fields.deletedAt),
}); });
const organizationWithMembers = await db.query.organization.findFirst({ const organizationWithMembers = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, organization.id), where: eq(drizzleDb.schemas.organization.id, organization.id),
with: {
members: {
with: { with: {
members: { user: true,
with: { },
user: true },
} },
} });
}
});
if (!organizationWithMembers) notFound(); if (!organizationWithMembers) notFound();
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full "> <PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
<div className="min-w-full md:min-w-fit "> <div className="min-w-full md:min-w-fit ">Organization settings</div>
Organization settings <div className="flex items-center gap-2 md:justify-between w-full ">
</div> <div className="flex items-center gap-2">
<div className="flex items-center gap-2 md:justify-between w-full "> {permissions.canManageSettings &&
<div className="flex items-center gap-2"> organization.slug !== "default" && (
{permissions.canManageSettings && organization.slug !== "default" && ( <EditOrganizationDialog
<EditOrganizationDialog organization={organizationWithMembers}
organization={organizationWithMembers} users={users}
users={users} currentUser={user}
currentUser={user} />
/> )}
)} </div>
</div> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> {permissions.canManageDangerZone &&
{permissions.canManageDangerZone && organization.slug !== "default" && ( organization.slug !== "default" && (
<DeleteOrganizationButton organizationSlug={organization.slug}/> <DeleteOrganizationButton
)} organizationSlug={organization.slug}
</div> />
</div> )}
</PageTitle> </div>
</PageHeader> </div>
<PageContent> </PageTitle>
<OrganizationTabs </PageHeader>
activeMember={activeMember} <PageContent>
organization={organization} <OrganizationTabs
notificationChannels={notificationChannels} activeMember={activeMember}
storageChannels={storageChannels} organization={organization}
/> notificationChannels={notificationChannels}
</PageContent> storageChannels={storageChannels}
</Page> />
) </PageContent>
</Page>
);
} }
@@ -1,60 +1,65 @@
"use client" "use client";
import {NotificationChannelWith} from "@/db/schema/09_notification-channel"; import { NotificationChannelWith } from "@/db/schema/09_notification-channel";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import {useState} from "react"; import { useState } from "react";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder"; import { EmptyStatePlaceholder } from "@/components/wrappers/common/empty-state-placeholder";
import {OrganizationWithMembers} from "@/db/schema/03_organization"; import { OrganizationWithMembers } from "@/db/schema/03_organization";
import {StorageChannelWith} from "@/db/schema/12_storage-channel"; import { StorageChannelWith } from "@/db/schema/12_storage-channel";
import {ChannelCard} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card"; import { ChannelCard } from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card";
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal"; import { ChannelAddEditModal } from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; import {
ChannelKind,
getChannelTextBasedOnKind,
} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
type ChannelsSectionProps = { type ChannelsSectionProps = {
channels: NotificationChannelWith[] | StorageChannelWith[], channels: NotificationChannelWith[] | StorageChannelWith[];
organizations: OrganizationWithMembers[], organizations: OrganizationWithMembers[];
kind: ChannelKind, kind: ChannelKind;
defaultStorageChannelId?: string | null | undefined defaultStorageChannelId?: string | null | undefined;
} };
export const ChannelsSection = ({ export const ChannelsSection = ({
organizations, organizations,
channels, channels,
kind, kind,
defaultStorageChannelId defaultStorageChannelId,
}: ChannelsSectionProps) => { }: ChannelsSectionProps) => {
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const channelText = getChannelTextBasedOnKind(kind);
const hasChannels = channels.length > 0;
const [isAddModalOpen, setIsAddModalOpen] = useState(false); return (
const channelText = getChannelTextBasedOnKind(kind) <div className="h-full">
const hasChannels = channels.length > 0 <ChannelAddEditModal
kind={kind}
open={isAddModalOpen}
return ( onOpenChangeAction={setIsAddModalOpen}
adminView={false}
trigger={false}
/>
{hasChannels ? (
<div className="h-full"> <div className="h-full">
<ChannelAddEditModal kind={kind} open={isAddModalOpen} onOpenChangeAction={setIsAddModalOpen} <CardsWithPagination
adminView={false} data={channels}
trigger={false}/> cardItem={ChannelCard}
{hasChannels ? ( cardsPerPage={8}
<div className="h-full"> numberOfColumns={2}
<CardsWithPagination adminView={true}
data={channels} organizations={organizations}
cardItem={ChannelCard} kind={kind}
cardsPerPage={8} defaultStorageChannelId={defaultStorageChannelId}
numberOfColumns={2} />
adminView={true}
organizations={organizations}
kind={kind}
defaultStorageChannelId={defaultStorageChannelId}
/>
</div>
) : (
<EmptyStatePlaceholder
text={`No ${channelText} channels configured yet`}
onClick={() => {
setIsAddModalOpen(true)
}}
className="h-full"
/>
)}
</div> </div>
); ) : (
} <EmptyStatePlaceholder
text={`No ${channelText} channels configured yet`}
onClick={() => {
setIsAddModalOpen(true);
}}
className="h-full"
/>
)}
</div>
);
};
@@ -1,49 +1,71 @@
"use client"; "use client";
import {useMutation, useQueryClient} from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import {useRouter} from "next/navigation"; import { useRouter } from "next/navigation";
import {toast} from "sonner"; import { toast } from "sonner";
import {backupButtonAction} from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action"; import { backupButtonAction } from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; import { Check, DatabaseZap, X } from "lucide-react";
import {Database, DatabaseZap} from "lucide-react"; import { useIsMobile } from "@/hooks/use-mobile";
import {useIsMobile} from "@/hooks/use-mobile"; import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
export type BackupButtonProps = { export type BackupButtonProps = {
databaseId: string; databaseId: string;
disable: boolean; disable: boolean;
}; };
export const BackupButton = (props: BackupButtonProps) => { export const BackupButton = (props: BackupButtonProps) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile() const isMobile = useIsMobile();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async (databaseId: string) => { mutationFn: async (databaseId: string) => {
const backup = await backupButtonAction(databaseId); const backup = await backupButtonAction(databaseId);
if (backup?.data?.success) { if (backup?.data?.success) {
toast.success(backup.data.actionSuccess?.message || "Backup created successfully!"); toast.success(
queryClient.invalidateQueries({queryKey: ["database-data", props.databaseId]}); backup.data.actionSuccess?.message || "Backup created successfully!",
router.refresh(); );
} else { queryClient.invalidateQueries({
toast.error(backup?.serverError || "Failed to create backup."); queryKey: ["database-data", props.databaseId],
} });
router.refresh();
} else {
toast.error(backup?.serverError || "Failed to create backup.");
}
},
});
const HandleAction = async () => {
await mutation.mutateAsync(props.databaseId);
};
return (
<ButtonWithConfirm
title="Create Backup"
description={"Are you sure you want to create a backup?"}
button={{
main: {
text: isMobile ? "" : "Backup",
variant: "default",
icon: <DatabaseZap />,
}, },
}); confirm: {
const HandleAction = async () => { className: "w-full",
await mutation.mutateAsync(props.databaseId); text: "Yes, create backup",
}; icon: <Check />,
variant: "default",
return ( onClick: async () => {
<ButtonWithLoading await HandleAction();
icon={<DatabaseZap/>} },
disabled={props.disable} },
isPending={mutation.isPending} cancel: {
size={"default"} className: "w-full",
onClick={async () => { text: "No, cancel",
await HandleAction(); icon: <X />,
}} variant: "outline",
>{isMobile ? "" : "Backup"}</ButtonWithLoading> },
); }}
isPending={mutation.isPending}
/>
);
}; };
@@ -1,144 +1,158 @@
"use client" "use client";
import {DatabaseBackupActionsModal} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-modal"; import { DatabaseBackupActionsModal } from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-modal";
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs"; import { DatabaseTabs } from "@/components/wrappers/dashboard/projects/database/database-tabs";
import {Setting} from "@/db/schema/01_setting"; import { Setting } from "@/db/schema/01_setting";
import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database"; import { BackupWith, DatabaseWith, Restoration } from "@/db/schema/07_database";
import {MemberWithUser} from "@/db/schema/03_organization"; import { MemberWithUser } from "@/db/schema/03_organization";
import {useBackupModal} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; import { useBackupModal } from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi"; import { DatabaseKpi } from "@/components/wrappers/dashboard/projects/database/database-kpi";
import {useQuery, useQueryClient} from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import {getDatabaseDataAction} from "@/components/wrappers/dashboard/database/backup/actions/get-data.action"; import { getDatabaseDataAction } from "@/components/wrappers/dashboard/database/backup/actions/get-data.action";
import {PageContent, PageDescription, PageTitle} from "@/features/layout/page"; import {
import {capitalizeFirstLetter} from "@/utils/text"; PageContent,
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet"; PageDescription,
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button"; PageTitle,
import {ChannelPoliciesModal} from "@/components/wrappers/dashboard/database/channels-policy/policy-modal"; } from "@/features/layout/page";
import {HardDrive, Megaphone} from "lucide-react"; import { capitalizeFirstLetter } from "@/utils/text";
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal"; import { RetentionPolicySheet } from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button"; import { CronButton } from "@/components/wrappers/dashboard/database/cron-button/cron-button";
import { ChannelPoliciesModal } from "@/components/wrappers/dashboard/database/channels-policy/policy-modal";
import { HardDrive, Megaphone } from "lucide-react";
import { ImportModal } from "@/components/wrappers/dashboard/database/import/import-modal";
import { BackupButton } from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
export type DatabaseContentProps = { export type DatabaseContentProps = {
settings: Setting, settings: Setting;
backups: BackupWith[], backups: BackupWith[];
restorations: Restoration[], restorations: Restoration[];
isAlreadyRestore: boolean, isAlreadyRestore: boolean;
database: DatabaseWith, database: DatabaseWith;
activeMember: MemberWithUser, activeMember: MemberWithUser;
totalBackups: number, totalBackups: number;
availableBackups: number, availableBackups: number;
successRate: number | null, successRate: number | null;
organizationId: string, organizationId: string;
activeOrganizationChannels: any[], activeOrganizationChannels: any[];
activeOrganizationStorageChannels: any[] activeOrganizationStorageChannels: any[];
} };
export const DatabaseContent = (props: DatabaseContentProps) => { export const DatabaseContent = (props: DatabaseContentProps) => {
const {} = useBackupModal(); const {} = useBackupModal();
const { data } = useQuery({
const {data} = useQuery({ queryKey: ["database-data", props.database.id],
queryKey: ["database-data", props.database.id], queryFn: async () => {
queryFn: async () => { const result = await getDatabaseDataAction({
const result = await getDatabaseDataAction({databaseId: props.database.id}); databaseId: props.database.id,
return result?.data; });
}, return result?.data;
initialData: { },
// TODO : to be patched initialData: {
// @ts-ignore // TODO : to be patched
database: { // @ts-ignore
...props.database, database: {
project: props.database.project ?? null, ...props.database,
}, project: props.database.project ?? null,
backups: props.backups, },
restorations: props.restorations, backups: props.backups,
activeOrganizationChannels: props.activeOrganizationChannels, restorations: props.restorations,
activeOrganizationStorageChannels: props.activeOrganizationStorageChannels, activeOrganizationChannels: props.activeOrganizationChannels,
stats: { activeOrganizationStorageChannels:
totalBackups: props.totalBackups, props.activeOrganizationStorageChannels,
availableBackups: props.availableBackups, stats: {
successRate: props.successRate
}
},
staleTime: 0,
gcTime: 0,
refetchInterval: 1000,
});
const database = data?.database ?? props.database;
const backups = data?.backups ?? props.backups;
const restorations = data?.restorations ?? props.restorations;
const activeOrganizationChannels = data?.activeOrganizationChannels ?? props.activeOrganizationChannels;
const activeOrganizationStorageChannels = data?.activeOrganizationStorageChannels ?? props.activeOrganizationStorageChannels;
const stats = data?.stats ?? {
totalBackups: props.totalBackups, totalBackups: props.totalBackups,
availableBackups: props.availableBackups, availableBackups: props.availableBackups,
successRate: props.successRate successRate: props.successRate,
}; },
},
staleTime: 0,
gcTime: 0,
refetchInterval: 1000,
});
const isAlreadyRestore = restorations.some((r) => r.status === "waiting"); const database = data?.database ?? props.database;
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing"); const backups = data?.backups ?? props.backups;
const restorations = data?.restorations ?? props.restorations;
const activeOrganizationChannels =
data?.activeOrganizationChannels ?? props.activeOrganizationChannels;
const activeOrganizationStorageChannels =
data?.activeOrganizationStorageChannels ??
props.activeOrganizationStorageChannels;
const stats = data?.stats ?? {
totalBackups: props.totalBackups,
availableBackups: props.availableBackups,
successRate: props.successRate,
};
const isMember = props.activeMember.role === "member"; const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
const isAlreadyBackup = backups.some(
(b) => b.status === "waiting" || b.status === "ongoing",
);
return ( const isMember = props.activeMember.role === "member";
<>
<div className="justify-between gap-2 sm:flex"> return (
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full "> <>
<div className="min-w-full md:min-w-fit "> <div className="justify-between gap-2 sm:flex">
{capitalizeFirstLetter(database.name)} <PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
</div> <div className="min-w-full md:min-w-fit ">
{!isMember && ( {capitalizeFirstLetter(database.name)}
<div className="flex items-center gap-2 md:justify-between w-full "> </div>
<div className="flex items-center gap-2"> {!isMember && (
<RetentionPolicySheet database={database}/> <div className="flex items-center gap-2 md:justify-between w-full ">
<CronButton database={database}/> <div className="flex items-center gap-2">
<ChannelPoliciesModal <RetentionPolicySheet database={database} />
database={database} <CronButton database={database} />
kind={"notification"} <ChannelPoliciesModal
icon={<Megaphone/>} database={database}
channels={activeOrganizationChannels} kind={"notification"}
organizationId={props.organizationId} icon={<Megaphone />}
/> channels={activeOrganizationChannels}
<ChannelPoliciesModal organizationId={props.organizationId}
database={database} />
icon={<HardDrive/>} <ChannelPoliciesModal
kind={"storage"} database={database}
channels={activeOrganizationStorageChannels} icon={<HardDrive />}
organizationId={props.organizationId} kind={"storage"}
/> channels={activeOrganizationStorageChannels}
<ImportModal database={database}/> organizationId={props.organizationId}
</div> />
<div className="flex items-center gap-2"> <ImportModal database={database} />
<BackupButton disable={isAlreadyBackup} databaseId={database.id}/> </div>
</div> <div className="flex items-center gap-2">
</div> <BackupButton
)} disable={isAlreadyBackup}
</PageTitle> databaseId={database.id}
/>
</div>
</div> </div>
)}
</PageTitle>
</div>
{database.description && ( {database.description && (
<PageDescription className="mt-5 sm:mt-0">{database.description}</PageDescription> <PageDescription className="mt-5 sm:mt-0">
)} {database.description}
</PageDescription>
)}
<PageContent className="flex flex-col w-full h-full"> <PageContent className="flex flex-col w-full h-full">
<DatabaseKpi <DatabaseKpi
successRate={stats.successRate} successRate={stats.successRate}
database={database} database={database}
availableBackups={stats.availableBackups} availableBackups={stats.availableBackups}
totalBackups={stats.totalBackups} totalBackups={stats.totalBackups}
/> />
<DatabaseBackupActionsModal/> <DatabaseBackupActionsModal />
<DatabaseTabs <DatabaseTabs
activeMember={props.activeMember} activeMember={props.activeMember}
settings={props.settings} settings={props.settings}
database={database} database={database}
isAlreadyRestore={isAlreadyRestore} isAlreadyRestore={isAlreadyRestore}
backups={backups} backups={backups}
restorations={restorations} restorations={restorations}
/> />
</PageContent> </PageContent>
</> </>
) );
} };
+156 -121
View File
@@ -2,148 +2,183 @@
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {MoreHorizontal, Trash2} from "lucide-react"; import { Check, MoreHorizontal, Trash2, X } from "lucide-react";
import { ReloadIcon } from "@radix-ui/react-icons"; import { ReloadIcon } from "@radix-ui/react-icons";
import { StatusBadge } from "@/components/wrappers/common/status-badge"; import { StatusBadge } from "@/components/wrappers/common/status-badge";
import {Restoration} from "@/db/schema/07_database"; import { Restoration } from "@/db/schema/07_database";
import {formatLocalizedDate} from "@/utils/date-formatting"; import { formatLocalizedDate } from "@/utils/date-formatting";
import {useMutation, useQueryClient} from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { import {
deleteRestoreAction, deleteRestoreAction,
rerunRestorationAction rerunRestorationAction,
} from "@/features/dashboard/restore/restore.action"; } from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner"; import { toast } from "sonner";
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom"; import { TooltipCustom } from "@/components/wrappers/common/tooltip-custom";
import {MemberWithUser} from "@/db/schema/03_organization"; import { MemberWithUser } from "@/db/schema/03_organization";
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
export function restoreColumns( export function restoreColumns(
isAlreadyRestore: boolean, isAlreadyRestore: boolean,
activeMember: MemberWithUser activeMember: MemberWithUser,
): ColumnDef<Restoration>[] { ): ColumnDef<Restoration>[] {
return[ return [
{ {
accessorKey: "id", accessorKey: "id",
header: "Reference", header: "Reference",
}, },
{ {
accessorKey: "createdAt", accessorKey: "createdAt",
header: "Created At", header: "Created At",
cell: ({ row }) => { cell: ({ row }) => {
return formatLocalizedDate(row.getValue("createdAt")) return formatLocalizedDate(row.getValue("createdAt"));
}, },
}, },
{ {
accessorKey: "status", accessorKey: "status",
header: "Status", header: "Status",
cell: ({ row }) => { cell: ({ row }) => {
return <StatusBadge status={row.getValue("status")} />; return <StatusBadge status={row.getValue("status")} />;
}, },
}, },
{ {
id: "actions", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
const status = row.getValue("status"); const status = row.getValue("status");
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const rowData: Restoration = row.original; const rowData: Restoration = row.original;
const mutationDeleteRestore = useMutation({
const mutationDeleteRestore = useMutation({ mutationFn: async () => {
mutationFn: async () => { const restoration = await deleteRestoreAction({
const restoration = await deleteRestoreAction({ restorationId: rowData.id,
restorationId: rowData.id,
});
// @ts-ignore
if (restoration.data.success) {
// @ts-ignore
toast.success(restoration.data.actionSuccess.message);
queryClient.invalidateQueries({queryKey: ["database-data", rowData.databaseId]});
} else {
// @ts-ignore
toast.error(restoration.data.actionError.message);
}
},
}); });
// @ts-ignore
const mutationRerunRestore = useMutation({ if (restoration.data.success) {
mutationFn: async () => { // @ts-ignore
const restoration = await rerunRestorationAction({ toast.success(restoration.data.actionSuccess.message);
restorationId: rowData.id, queryClient.invalidateQueries({
}); queryKey: ["database-data", rowData.databaseId],
// @ts-ignore });
if (restoration.data.success) { } else {
// @ts-ignore // @ts-ignore
toast.success(restoration.data.actionSuccess.message); toast.error(restoration.data.actionError.message);
queryClient.invalidateQueries({queryKey: ["database-data", rowData.databaseId]});
} else {
// @ts-ignore
toast.error(restoration.data.actionError.message);
}
},
});
const handleDelete = async () => {
await mutationDeleteRestore.mutateAsync();
};
const handleRerunRestore = async () => {
await mutationRerunRestore.mutateAsync();
} }
},
});
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],
});
} else {
// @ts-ignore
toast.error(restoration.data.actionError.message);
}
},
});
return ( const handleDelete = async () => {
<> await mutationDeleteRestore.mutateAsync();
{activeMember.role != "member" && ( };
<DropdownMenu>
<DropdownMenuTrigger asChild> const handleRerunRestore = async () => {
<Button variant="ghost" className="h-8 w-8 p-0" type="button" onClick={(e) => e.stopPropagation()}> await mutationRerunRestore.mutateAsync();
<span className="sr-only">Open menu</span> };
<MoreHorizontal className="h-4 w-4"/>
</Button> return (
</DropdownMenuTrigger> <>
<DropdownMenuContent align="end"> {activeMember.role !== "member" && (
<DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenu>
{rowData.backupStorageId && ( <DropdownMenuTrigger asChild>
<TooltipCustom disabled={isAlreadyRestore} text="Already a restoration waiting"> <Button
<DropdownMenuItem variant="ghost"
disabled={mutationRerunRestore.isPending || isAlreadyRestore} className="h-8 w-8 p-0"
onClick={async () => { type="button"
await handleRerunRestore(); onClick={(e) => e.stopPropagation()}
}} >
> <span className="sr-only">Open menu</span>
<ReloadIcon/> Rerun <MoreHorizontal className="h-4 w-4" />
</DropdownMenuItem> </Button>
</TooltipCustom> </DropdownMenuTrigger>
)} <DropdownMenuContent align="end">
<DropdownMenuSeparator/> <DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem {rowData.backupStorageId && (
disabled={status == "waiting"} <TooltipCustom
className="text-red-600" disabled={isAlreadyRestore}
onClick={async () => { text="Already a restoration waiting"
await handleDelete(); >
}} <DropdownMenuItem
> disabled={
<Trash2/> Delete mutationRerunRestore.isPending || isAlreadyRestore
</DropdownMenuItem> }
</DropdownMenuContent> onClick={async () => {
</DropdownMenu> 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>
)}
</>
);
},
}, },
]; ];
} }