Merge pull request #107 from Portabase/feat/sensible-actions

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