mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: adding system settings page. And did some refactoring.
This commit is contained in:
@@ -2,6 +2,10 @@ import {PageParams} from "@/types/next";
|
|||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
|
import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/settings-tabs";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
@@ -9,11 +13,18 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!settings) {
|
const storageChannels = await db.query.storageChannel.findMany({
|
||||||
|
with: {
|
||||||
|
organizations: true
|
||||||
|
},
|
||||||
|
where: isNull(drizzleDb.schemas.storageChannel.organizationId),
|
||||||
|
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
|
||||||
|
}) as StorageChannelWith[]
|
||||||
|
|
||||||
|
if (!settings || !storageChannels ) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader className="flex flex-col">
|
<PageHeader className="flex flex-col">
|
||||||
@@ -22,6 +33,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<SettingsTabs storageChannels={storageChannels} settings={settings} />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
|
|||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {
|
|
||||||
DeleteOrganizationButton
|
|
||||||
} from "@/components/wrappers/dashboard/organization/delete-organization/delete-organization-button";
|
|
||||||
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-button-settings/edit-button-settings";
|
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||||
|
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
||||||
|
import {
|
||||||
|
EditButtonSettings
|
||||||
|
} from "@/components/wrappers/dashboard/organization/settings/edit-button-settings/edit-button-settings";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {eventEmitter} from "../../../events/route";
|
import {eventEmitter} from "../../../events/route";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {Backup} from "@/db/schema/07_database";
|
import {Backup} from "@/db/schema/07_database";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {env} from "@/env.mjs";
|
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {decryptedDump, getFileExtension} from "./helpers";
|
import {decryptedDump, getFileExtension} from "./helpers";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: R
|
|||||||
...classNames,
|
...classNames,
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
|
// @ts-ignore
|
||||||
IconLeft: ({ className, children: _children, ...props }) => <ChevronLeftIcon className={cn("size-4", className)} {...props} />,
|
IconLeft: ({ className, children: _children, ...props }) => <ChevronLeftIcon className={cn("size-4", className)} {...props} />,
|
||||||
|
// @ts-ignore
|
||||||
IconRight: ({ className, children: _children, ...props }) => <ChevronRightIcon className={cn("size-4", className)} {...props} />,
|
IconRight: ({ className, children: _children, ...props }) => <ChevronRightIcon className={cn("size-4", className)} {...props} />,
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ export const ButtonWithLoading = ({
|
|||||||
{isPending && <Loader2 className="mr-2 animate-spin" size={16} />}
|
{isPending && <Loader2 className="mr-2 animate-spin" size={16} />}
|
||||||
{children && children}
|
{children && children}
|
||||||
<>{icon ? icon : null}</>
|
<>{icon ? icon : null}</>
|
||||||
{/*{icon && <span className="ml-2">{icon}</span>}*/}
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-3
@@ -13,16 +13,14 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle
|
DialogTitle
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
|
||||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {MemberRoleType} from "@/types/common";
|
import {MemberRoleType} from "@/types/common";
|
||||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||||
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action";
|
|
||||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
|
||||||
import {
|
import {
|
||||||
updateMemberRoleAdminAction
|
updateMemberRoleAdminAction
|
||||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/role-member.action";
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/role-member.action";
|
||||||
|
import {RoleSchemaMember} from "@/components/wrappers/dashboard/organization/settings/member.schema";
|
||||||
|
|
||||||
type OrganizationMemberChangeRoleModalProps = {
|
type OrganizationMemberChangeRoleModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
+1
-1
@@ -3,11 +3,11 @@ import {userAction} from "@/lib/safe-actions/actions";
|
|||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {Member} from "better-auth/plugins";
|
import {Member} from "better-auth/plugins";
|
||||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
|
||||||
import {db as dbClient} from "@/db";
|
import {db as dbClient} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {RoleSchemaMember} from "@/components/wrappers/dashboard/organization/settings/member.schema";
|
||||||
|
|
||||||
|
|
||||||
export const updateMemberRoleAdminAction = userAction.schema(
|
export const updateMemberRoleAdminAction = userAction.schema(
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
|
||||||
import {Button} from "@/components/ui/button";
|
|
||||||
import {Download} from "lucide-react";
|
|
||||||
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
|
|
||||||
export type AdminSettingsTabProps = {};
|
|
||||||
|
|
||||||
export const AdminSettingsSection = (props: AdminSettingsTabProps) => {
|
|
||||||
|
|
||||||
const handleDownloadKey = async () => {
|
|
||||||
|
|
||||||
let url: string = "";
|
|
||||||
const data = await getFileUrlPresignedLocal({dir: "private/keys/", fileName: "server_public.pem"})
|
|
||||||
if (data?.data?.success) {
|
|
||||||
url = data.data.value ?? "";
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
const errorMessage = data?.data?.actionError?.message || "Failed to get file!";
|
|
||||||
toast.error(errorMessage);
|
|
||||||
}
|
|
||||||
window.open(url, "_self");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Instance settings</CardTitle>
|
|
||||||
<CardDescription>Manage portabase settings</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">Download Public Key</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Used for encrypting communications with this instance.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button onClick={handleDownloadKey} variant="outline" size="sm">
|
|
||||||
<Download className="h-4 w-4 mr-2"/>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {useRouter, useSearchParams} from "next/navigation";
|
||||||
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
|
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
|
||||||
|
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
|
||||||
|
export type SettingsTabsProps = {
|
||||||
|
settings: Setting
|
||||||
|
storageChannels: StorageChannelWith[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsTabs = ({settings, storageChannels}: SettingsTabsProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "email");
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newTab = searchParams.get("tab") ?? "email";
|
||||||
|
setTab(newTab);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const handleChangeTab = (value: string) => {
|
||||||
|
router.push(`?tab=${value}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full mt-3">
|
||||||
|
<Tabs className="h-full" value={tab} onValueChange={handleChangeTab}>
|
||||||
|
<TabsList className='bg-background rounded-none border-b p-0 min-w-48'>
|
||||||
|
<TabsTrigger
|
||||||
|
value="email"
|
||||||
|
className='bg-background data-[state=active]:border-primary dark:data-[state=active]:border-primary h-full rounded-none border-0 border-b-2 border-transparent data-[state=active]:shadow-none'
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="storage"
|
||||||
|
className='bg-background data-[state=active]:border-primary dark:data-[state=active]:border-primary h-full rounded-none border-0 border-b-2 border-transparent data-[state=active]:shadow-none'
|
||||||
|
>
|
||||||
|
Default storage
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent className="h-full" value="email">
|
||||||
|
<SettingsEmailSection settings={settings}/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent className="h-full" value="storage">
|
||||||
|
<SettingsStorageSection storageChannels={storageChannels} settings={settings}/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
+68
-72
@@ -1,106 +1,102 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||||
import {Info, ShieldCheck} from "lucide-react";
|
import {Info} from "lucide-react";
|
||||||
import {Switch} from "@/components/ui/switch";
|
|
||||||
import {Label} from "@/components/ui/label";
|
|
||||||
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/storage-s3-form";
|
|
||||||
import {useState} from "react";
|
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
|
||||||
import {checkConnexionToS3} from "@/features/upload/public/upload.action";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
|
import {Form, FormField, FormItem, useZodForm} from "@/components/ui/form";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||||
|
import {useMutation} from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
DefaultStorageSchema,
|
||||||
|
DefaultStorageType
|
||||||
|
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.schema";
|
||||||
|
import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
import {
|
import {
|
||||||
updateStorageSettingsAction
|
updateStorageSettingsAction
|
||||||
} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.action";
|
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.action";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import {toast} from "sonner";
|
||||||
import {S3FormType} from "@/components/wrappers/dashboard/admin/settings/storage/storage-s3/s3-form.schema";
|
|
||||||
|
|
||||||
export type SettingsStorageSectionProps = {
|
export type SettingsStorageSectionProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
|
storageChannels: StorageChannelWith[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsStorageSection = (props: SettingsStorageSectionProps) => {
|
export const SettingsStorageSection = ({settings, storageChannels}: SettingsStorageSectionProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const mutation = useMutation({
|
const form = useZodForm({
|
||||||
mutationFn: async () => {
|
schema: DefaultStorageSchema,
|
||||||
const result = await checkConnexionToS3();
|
defaultValues: {
|
||||||
if (result.error) {
|
storageChannelId: settings.defaultStorageChannelId ?? undefined
|
||||||
toast.error("An error occured during the connexion !");
|
|
||||||
} else {
|
|
||||||
toast.success("Connexion succeed!");
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
|
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () => updateStorageSettingsAction({name: "system", data: {storage: isSwitched ? "s3" : "local"}}),
|
mutationFn: async (values: DefaultStorageType) => {
|
||||||
onSuccess: () => {
|
const result = await updateStorageSettingsAction({name: "system", data: values})
|
||||||
toast.success(`Settings updated successfully.`);
|
const inner = result?.data;
|
||||||
|
|
||||||
|
if (inner?.success) {
|
||||||
|
toast.success(inner.actionSuccess?.message);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
},
|
} else {
|
||||||
onError: () => {
|
toast.error(inner?.actionError?.message);
|
||||||
toast.error(`An error occurred while updating settings information.`);
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const HandleSwitchStorage = async () => {
|
|
||||||
setIsSwitched(!isSwitched);
|
|
||||||
await updateMutation.mutateAsync();
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractS3FormValues = (settings: Setting): S3FormType | undefined => {
|
|
||||||
if (!settings.s3EndPointUrl) return undefined;
|
|
||||||
return {
|
|
||||||
s3EndPointUrl: settings.s3EndPointUrl,
|
|
||||||
s3AccessKeyId: settings.s3AccessKeyId!,
|
|
||||||
s3SecretAccessKey: settings.s3SecretAccessKey!,
|
|
||||||
S3BucketName: settings.S3BucketName!,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<Alert className="mt-3">
|
<Alert className="mt-3">
|
||||||
<Info className="h-4 w-4"/>
|
<Info className="h-4 w-4"/>
|
||||||
<AlertTitle>Informations</AlertTitle>
|
<AlertTitle>Informations</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Actually you can only store you data in one place : s3 compatible or in local. For exemple you
|
The default storage channel will be used by default to store your backups if no storage policy is
|
||||||
cannot choose to store images in one place
|
configured at the database level. </AlertDescription>
|
||||||
and backups files in another.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
<div className="flex flex-col h-full py-4 ">
|
<div className="flex flex-col h-full py-4 ">
|
||||||
<div className="flex items-center justify-between space-x-2">
|
<Form
|
||||||
<div className="flex items-center space-x-2">
|
form={form}
|
||||||
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
|
onSubmit={async (values) => {
|
||||||
<Switch
|
await mutation.mutateAsync(values);
|
||||||
checked={isSwitched}
|
|
||||||
onCheckedChange={async () => {
|
|
||||||
await HandleSwitchStorage();
|
|
||||||
}}
|
}}
|
||||||
id="storage-mode"
|
>
|
||||||
/>
|
<div className="flex items-center gap-3">
|
||||||
</div>
|
<FormField
|
||||||
<div>
|
control={form.control}
|
||||||
<ButtonWithLoading
|
name="storageChannelId"
|
||||||
size={"default"}
|
render={({field}) => (
|
||||||
disabled={!isSwitched}
|
<FormItem className="flex items-center justify-center">
|
||||||
isPending={mutation.isPending}
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
onClick={async () => {
|
<SelectTrigger className="w-90 h-full mb-0">
|
||||||
await mutation.mutateAsync();
|
<SelectValue placeholder="Select a default storage channel"/>
|
||||||
}}
|
</SelectTrigger>
|
||||||
icon={<ShieldCheck/>}>Test connexion</ButtonWithLoading>
|
<SelectContent>
|
||||||
</div>
|
{storageChannels.map((channel) => (
|
||||||
</div>
|
<SelectItem key={channel.id} value={channel.id}>
|
||||||
{isSwitched && (
|
<div className="flex items-center gap-2">
|
||||||
<div className="mt-5">
|
{getChannelIcon(channel.provider)}
|
||||||
<StorageS3Form defaultValues={extractS3FormValues(props.settings)}/>
|
<span className="font-medium">{channel.name}</span>
|
||||||
|
<span
|
||||||
|
className="text-[9px] uppercase bg-secondary px-1.5 py-0.5 rounded">
|
||||||
|
{channel.provider}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
<ButtonWithLoading type="submit">
|
||||||
|
Confirm
|
||||||
|
</ButtonWithLoading>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
|
import {z} from "zod";
|
||||||
|
import {DefaultStorageSchema} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.schema";
|
||||||
|
|
||||||
|
export const updateStorageSettingsAction = userAction
|
||||||
|
.schema(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
data: DefaultStorageSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.action(async ({parsedInput}): Promise<ServerActionResult<Setting>> => {
|
||||||
|
const {name, data} = parsedInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const [updatedSettings] = await db
|
||||||
|
.update(drizzleDb.schemas.setting)
|
||||||
|
.set({
|
||||||
|
defaultStorageChannelId: data.storageChannelId,
|
||||||
|
})
|
||||||
|
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||||
|
.returning();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: updatedSettings,
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Settings successfully updated",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "Failed update settings.",
|
||||||
|
status: 500,
|
||||||
|
cause: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const DefaultStorageSchema = z.object({
|
||||||
|
storageChannelId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DefaultStorageType = z.infer<typeof DefaultStorageSchema>;
|
||||||
@@ -41,10 +41,6 @@ export const ChannelPoliciesModal = ({icon, kind, database, channels, organizati
|
|||||||
const activeAlertPolicies = database.alertPolicies?.filter((policy) => channelsIds.includes(policy.notificationChannelId));
|
const activeAlertPolicies = database.alertPolicies?.filter((policy) => channelsIds.includes(policy.notificationChannelId));
|
||||||
const activeStoragePolicies = database.storagePolicies?.filter((policy) => channelsIds.includes(policy.storageChannelId));
|
const activeStoragePolicies = database.storagePolicies?.filter((policy) => channelsIds.includes(policy.storageChannelId));
|
||||||
|
|
||||||
console.log(channels);
|
|
||||||
console.log(database.storagePolicies);
|
|
||||||
console.log("activeAlertPolicies", activeAlertPolicies);
|
|
||||||
console.log("activeStoragePolicies", activeStoragePolicies);
|
|
||||||
|
|
||||||
const activePolicies = kind === "notification" ? activeAlertPolicies : activeStoragePolicies;
|
const activePolicies = kind === "notification" ? activeAlertPolicies : activeStoragePolicies;
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const AdvancedCronSelect = ({
|
|||||||
</Label>
|
</Label>
|
||||||
{!isAdvanced ? (
|
{!isAdvanced ? (
|
||||||
<Select
|
<Select
|
||||||
|
// @ts-ignore
|
||||||
id={id}
|
id={id}
|
||||||
className="col-span-4"
|
className="col-span-4"
|
||||||
value={defaultValue}
|
value={defaultValue}
|
||||||
|
|||||||
+2
-2
@@ -13,8 +13,8 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { updateMemberRoleAction } from "@/components/wrappers/dashboard/settings/update-member.action";
|
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/organization/settings/update-member.action";
|
||||||
import { RoleSchemaMember } from "@/components/wrappers/dashboard/settings/member.schema";
|
import {RoleSchemaMember} from "@/components/wrappers/dashboard/organization/settings/member.schema";
|
||||||
|
|
||||||
export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
||||||
{
|
{
|
||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {organizationMemberColumns} from "@/components/wrappers/dashboard/settings/columns-organization-members";
|
import {
|
||||||
|
organizationMemberColumns
|
||||||
|
} from "@/components/wrappers/dashboard/organization/settings/columns-organization-members";
|
||||||
|
|
||||||
interface SettingsOrganizationMembersTableProps {
|
interface SettingsOrganizationMembersTableProps {
|
||||||
organization: OrganizationWithMembers
|
organization: OrganizationWithMembers
|
||||||
+1
-1
@@ -4,8 +4,8 @@ import {z} from "zod";
|
|||||||
import {auth} from "@/lib/auth/auth";
|
import {auth} from "@/lib/auth/auth";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {Member} from "better-auth/plugins";
|
import {Member} from "better-auth/plugins";
|
||||||
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
|
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
|
import {RoleSchemaMember} from "@/components/wrappers/dashboard/organization/settings/member.schema";
|
||||||
|
|
||||||
|
|
||||||
export const updateMemberRoleAction = userAction.schema(
|
export const updateMemberRoleAction = userAction.schema(
|
||||||
@@ -4,18 +4,18 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {useRouter, useSearchParams} from "next/navigation";
|
import {useRouter, useSearchParams} from "next/navigation";
|
||||||
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {
|
|
||||||
SettingsOrganizationMembersTable
|
|
||||||
} from "@/components/wrappers/dashboard/settings/settings-organization-members-table";
|
|
||||||
import {
|
|
||||||
OrganizationNotifiersTab
|
|
||||||
} from "@/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-notifiers-tab";
|
|
||||||
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
import {NotificationChannel} from "@/db/schema/09_notification-channel";
|
||||||
import {useOrganizationPermissions} from "@/hooks/use-organization-permissions";
|
import {useOrganizationPermissions} from "@/hooks/use-organization-permissions";
|
||||||
|
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
||||||
|
import {
|
||||||
|
SettingsOrganizationMembersTable
|
||||||
|
} from "@/components/wrappers/dashboard/organization/settings/settings-organization-members-table";
|
||||||
|
import {
|
||||||
|
OrganizationNotifiersTab
|
||||||
|
} from "@/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-notifiers-tab";
|
||||||
import {
|
import {
|
||||||
OrganizationStoragesTab
|
OrganizationStoragesTab
|
||||||
} from "@/components/wrappers/dashboard/organization/tabs/organization-notifiers-tab/organization-storages-tab";
|
} from "@/components/wrappers/dashboard/organization/tabs/organization-channels-tab/organization-storages-tab";
|
||||||
import {StorageChannel} from "@/db/schema/12_storage-channel";
|
|
||||||
|
|
||||||
export type OrganizationTabsProps = {
|
export type OrganizationTabsProps = {
|
||||||
organization: OrganizationWithMembers;
|
organization: OrganizationWithMembers;
|
||||||
|
|||||||
@@ -27,18 +27,25 @@ export async function storeBackupFiles(
|
|||||||
? [{
|
? [{
|
||||||
id: null,
|
id: null,
|
||||||
storageChannelId: settings.storageChannel.id,
|
storageChannelId: settings.storageChannel.id,
|
||||||
enabled: true
|
enabled: settings.storageChannel.enabled,
|
||||||
}]
|
}]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
console.log(database.storagePolicies);
|
const enabledPolicies = database.storagePolicies?.filter(p => p.enabled) ?? [];
|
||||||
|
|
||||||
const policies = (database.storagePolicies?.filter(p => p.enabled) || defaultPolicy);
|
const policies = enabledPolicies.length > 0
|
||||||
|
? enabledPolicies
|
||||||
console.log("Policies", policies);
|
: defaultPolicy;
|
||||||
|
|
||||||
|
console.debug("Policies", policies);
|
||||||
|
|
||||||
if (!policies.length) {
|
if (!policies.length) {
|
||||||
|
await db
|
||||||
|
.update(drizzleDb.schemas.backup)
|
||||||
|
.set(withUpdatedAt({
|
||||||
|
status: "failed",
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +72,6 @@ export async function storeBackupFiles(
|
|||||||
data: {path, file},
|
data: {path, file},
|
||||||
};
|
};
|
||||||
|
|
||||||
// const result = await dispatchStorage(input, policy.id);
|
|
||||||
let result: StorageResult;
|
let result: StorageResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -93,8 +99,6 @@ export async function storeBackupFiles(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(results);
|
|
||||||
|
|
||||||
const backupStatus = results.some(r => r.success) ? "success" : "failed";
|
const backupStatus = results.some(r => r.success) ? "success" : "failed";
|
||||||
|
|
||||||
await db
|
await db
|
||||||
|
|||||||
Reference in New Issue
Block a user