mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
update: sensible action button confirm
This commit is contained in:
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user