Merge remote-tracking branch 'origin/main'

# Conflicts:
#	src/components/wrappers/dashboard/Organization/organization-combobox.tsx
This commit is contained in:
charles-gauthereau
2024-12-16 12:03:16 +01:00
93 changed files with 207 additions and 265 deletions
@@ -0,0 +1,163 @@
"use client";
import {Card, CardContent} from "@/components/ui/card";
import {
FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form"
import {Button} from "@/components/ui/button";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import {
EmailFormSchema,
EmailFormType
} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
import Link from "next/link";
import {PasswordInput} from "@/components/wrappers/auth/PaswordInput/password-input";
import {
updateEmailSettingsAction
} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
export type EmailFormProps = {
defaultValues?: EmailFormType;
}
export const EmailForm = (props: EmailFormProps) => {
const isCreate = !Boolean(props.defaultValues)
const form = useZodForm({
schema: EmailFormSchema,
defaultValues: props.defaultValues,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: EmailFormType) => {
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values})
const data = updateEmailSettings?.data?.data
if (updateEmailSettings?.serverError || !data) {
console.log(updateEmailSettings?.serverError);
toast.error(updateEmailSettings?.serverError);
return;
}
toast.success(`Success updating email informations`);
router.refresh()
}
})
return (
<TooltipProvider>
<Card>
<CardContent>
<Form form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="smtpFrom"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>From Email *</FormLabel>
<FormControl>
<Input
placeholder={"exemple@portabase.com"} {...field} />
</FormControl>
<FormDescription>{"The email from where the email will be send"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtpHost"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Server Host *</FormLabel>
<FormControl>
<Input
placeholder={"ssl0.ovh.net"} {...field} />
</FormControl>
<FormDescription>{"Your email server host"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtpPort"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Server Port *</FormLabel>
<FormControl>
<Input
placeholder={"465"} {...field} />
</FormControl>
<FormDescription>{"Your email server port (send)"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtpPassword"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput placeholder="Password" {...field}/>
</FormControl>
<FormDescription>{"Your email server password"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtpUser"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>User Email *</FormLabel>
<FormControl>
<Input
placeholder={"exemple@portabase.com"} {...field} />
</FormControl>
<FormDescription>{"The email server user"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button>
Save
</Button>
</div>
</Form>
</CardContent>
</Card>
</TooltipProvider>
)
}
@@ -0,0 +1,31 @@
"use server"
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {prisma} from "@/prisma";
import {EmailFormSchema} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
export const updateEmailSettingsAction = userAction
.schema(
z.object({
name: z.string(),
data: EmailFormSchema,
}
)
)
.action(async ({parsedInput, ctx}) => {
console.log("parsedInput", parsedInput.data)
const updatedSettings = await prisma.settings.update({
where: {
name: parsedInput.name,
},
data: parsedInput.data,
})
return {
data: updatedSettings,
}
})
@@ -0,0 +1,11 @@
import {z} from "zod";
export const EmailFormSchema = z.object({
smtpPassword: z.string(),
smtpFrom: z.string(),
smtpHost: z.string(),
smtpPort: z.string(),
smtpUser: z.string(),
});
export type EmailFormType = z.infer<typeof EmailFormSchema>;
@@ -0,0 +1,61 @@
import {EmailForm} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/EmailForm";
import {Settings} from "@prisma/client";
import {Send} from "lucide-react";
import {ButtonWithLoading} from "@/components/wrappers/common/button/ButtonWithLoading/ButtonWithLoading";
import {useMutation} from "@tanstack/react-query";
import {sendEmail} from "@/utils/email-helper";
import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
import {render} from "@react-email/render";
import {toast} from "sonner";
import HelloEmail from "../../../../../../emails/HelloEmail";
export type SettingsEmailTabProps = {
settings: Settings
}
export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
const mutation = useMutation({
mutationFn: async () => {
const email = await sendEmail({
to: props.settings.smtpUser,
subject: "Portabase",
html: await render(TestEmailSettings(),{})
});
if(email.response){
toast.success("Test Email Successfully sent !");
}
}
})
const handleSendMailTest = async () => {
await mutation.mutateAsync()
}
return (
<div className="flex flex-col h-full py-4">
<div className="flex gap-4 h-fit justify-between">
<h1>Settings for Portabase email setup</h1>
{props.settings.smtpFrom && (
<ButtonWithLoading
isPending={mutation.isPending}
onClick={async () => {
await handleSendMailTest()
}}
icon={<Send/>}
text="Send email test"
size="default"
/>
)}
</div>
<div className="mt-5">
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings : null}/>
</div>
</div>
)
}
@@ -0,0 +1,99 @@
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Info, Send, ShieldCheck} from "lucide-react";
import {Switch} from "@/components/ui/switch";
import {Label} from "@/components/ui/label";
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/StorageS3Form";
import {useState} from "react";
import {Settings} from "@prisma/client";
import {ButtonWithLoading} from "@/components/wrappers/common/button/ButtonWithLoading/ButtonWithLoading";
import {useMutation} from "@tanstack/react-query";
import {checkConnexionToS3} from "@/features/upload/public/upload.action";
import {toast} from "sonner";
import {updateUserAction} from "@/components/wrappers/dashboard/Profile/UserForm/user-form.action";
import {useRouter} from "next/navigation";
import {
updateStorageSettingsAction
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
export type SettingsStorageTabProps = {
settings: Settings
}
export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
const router = useRouter()
const mutation = useMutation({
mutationFn: async () => {
const result = await checkConnexionToS3()
if(result.error){
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({
mutationFn: () => updateStorageSettingsAction({name: "system", data: {storage: isSwitched ? "s3": "local"}}),
onSuccess: () => {
toast.success(`Settings updated successfully.`);
router.refresh()
},
onError: () => {
toast.error(`An error occurred while updating settings information.`);
},
});
const HandleSwitchStorage = async () => {
setIsSwitched(!isSwitched);
await updateMutation.mutateAsync()
}
return (
<div className="flex flex-col h-full py-4">
<h1>Settings for Portabase storage</h1>
<Alert className="mt-3">
<Info className="h-4 w-4"/>
<AlertTitle>Informations</AlertTitle>
<AlertDescription>
Actually you can only store you data in one place : s3 compatible or in local.
For exemple you cannot choose to store images in one place and .dump files in another.
</AlertDescription>
</Alert>
<div className="flex flex-col h-full py-4 ">
<div className="flex items-center justify-between space-x-2">
<div className="flex items-center space-x-2">
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
<Switch
checked={isSwitched}
onCheckedChange={async () => {
await HandleSwitchStorage()
}}
id="storage-mode"/>
</div>
<div>
<ButtonWithLoading
size={"default"}
disabled={!isSwitched}
isPending={mutation.isPending}
onClick={async () => {
await mutation.mutateAsync()
}}
icon={<ShieldCheck />}
text="Test connexion"
/>
</div>
</div>
{isSwitched && (
<div className="mt-5">
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null}/>
</div>
)}
</div>
</div>
)
}
@@ -0,0 +1,131 @@
"use client";
import {Card, CardContent} from "@/components/ui/card";
import {
FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form"
import {Button} from "@/components/ui/button";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import {
S3FormSchema,
S3FormType
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
import {
updateS3SettingsAction
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
export type S3FormProps = {
defaultValues?: S3FormType;
}
export const StorageS3Form = (props: S3FormProps) => {
const form = useZodForm({
schema: S3FormSchema,
defaultValues: props.defaultValues,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: S3FormType) => {
console.log(values)
const updateS3Settings = await updateS3SettingsAction({name: "system", data: values})
const data = updateS3Settings?.data?.data
if (updateS3Settings?.serverError || !data) {
console.log(updateS3Settings?.serverError);
toast.error(updateS3Settings?.serverError);
return;
}
toast.success(`Success updating storage informations`);
router.refresh()
}
})
return (
<TooltipProvider>
<Card>
<CardContent>
<Form form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="s3EndPointUrl"
render={({field}) => (
<FormItem>
<FormLabel>Endpoint Url *</FormLabel>
<FormControl>
<Input
placeholder={"s3.eu-west-3.amazonaws.com"} {...field} />
</FormControl>
<FormDescription>{"Your s3 compatible url"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="s3AccessKeyId"
render={({field}) => (
<FormItem>
<FormLabel>Access Key *</FormLabel>
<FormControl>
<Input
placeholder={"The access key token"} {...field} />
</FormControl>
<FormDescription>{"Add your access key"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="s3SecretAccessKey"
render={({field}) => (
<FormItem>
<FormLabel>Secret Key *</FormLabel>
<FormControl>
<Input
placeholder={"The secret key token"} {...field} />
</FormControl>
<FormDescription>{"Add your secret key"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="S3BucketName"
render={({field}) => (
<FormItem>
<FormLabel>Bucket name *</FormLabel>
<FormControl>
<Input
placeholder={"my-bucket"} {...field} />
</FormControl>
<FormDescription>{"The bucket name where you want to store your data"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button>
Save
</Button>
</div>
</Form>
</CardContent>
</Card>
</TooltipProvider>
)
}
@@ -0,0 +1,51 @@
"use server"
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {prisma} from "@/prisma";
import {
S3FormSchema,
StorageSwitchSchema
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
export const updateS3SettingsAction = userAction
.schema(
z.object({
name: z.string(),
data: S3FormSchema,
}
)
)
.action(async ({parsedInput, ctx}) => {
const updatedSettings = await prisma.settings.update({
where: {
name: parsedInput.name,
},
data: parsedInput.data,
})
return {
data: updatedSettings,
}
})
export const updateStorageSettingsAction = userAction
.schema(
z.object({
name: z.string(),
data: StorageSwitchSchema,
}
)
)
.action(async ({parsedInput, ctx}) => {
const updatedSettings = await prisma.settings.update({
where: {
name: parsedInput.name,
},
data: parsedInput.data,
})
return {
data: updatedSettings,
}
})
@@ -0,0 +1,17 @@
import {z} from "zod";
export const S3FormSchema = z.object({
s3EndPointUrl: z.string(),
s3AccessKeyId: z.string(),
s3SecretAccessKey: z.string(),
S3BucketName: z.string(),
});
export type S3FormType = z.infer<typeof S3FormSchema>;
export const StorageSwitchSchema = z.object({
storage: z.string(),
})
export type StorageType= z.infer<typeof StorageSwitchSchema>;
@@ -0,0 +1,42 @@
"use client"
import {User, Settings} from "@prisma/client";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/AdminEmailTab/SettingsEmailTab";
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/AdminStorageTab/SettingsStorageTab";
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-table";
export type AdminTabsProps = {
currentUser: User;
users: User[];
settings: Settings;
}
export const AdminTabs = (props: AdminTabsProps) => {
const {currentUser, users, settings} = props;
return (
<Tabs defaultValue="users">
<TabsList className="w-full">
<TabsTrigger className="w-full " value="users">Users</TabsTrigger>
<TabsTrigger className="w-full " value="email">Email</TabsTrigger>
<TabsTrigger className="w-full " value="storage">Storage</TabsTrigger>
</TabsList>
<TabsContent value="users">
<AdminUsersTable currentUser={currentUser} users={users}/>
</TabsContent>
<TabsContent value="email">
<SettingsEmailTab settings={settings}/>
</TabsContent>
<TabsContent value="storage">
<SettingsStorageTab settings={settings}/>
</TabsContent>
</Tabs>
)
}
@@ -0,0 +1,92 @@
import {flexRender, Row, RowData} from "@tanstack/react-table";
import {User} from "@prisma/client";
import {DataTableWithPagination} from "@/components/wrappers/table/data-table-with-pagination";
import {usersColumns} from "@/components/wrappers/dashboard/Settings/SettingsUsersTab/columns-users";
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
import {cn} from "@/lib/utils";
export type AdminUsersTableProps = {
currentUser: User;
users: User[]
}
export const AdminUsersTable = (props: AdminUsersTableProps) => {
const {currentUser, users} = props;
return (
<div className="flex flex-col h-full py-4">
<div className="flex gap-4 h-fit justify-between">
<h1>List of Portabase's users</h1>
</div>
<div className="mt-5">
<DataTableWithPagination
columns={usersColumns}
data={users}
DataTable={UsersDataTable}
dataTableProps={{currentUser}}
/>
</div>
</div>
)
}
export type usersDataTableProps = {
currentUser: User;
table: any,
}
export const UsersDataTable = ({currentUser, table}: usersDataTableProps) => {
return (
<div className="rounded-md border w-full ">
<Table className="w-full">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row: Row<User>) => (
<TableRow
className={cn(row.original.id === currentUser.id ? "opacity-40 pointer-events-none" : "")}
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))) : (
<TableRow>
<TableCell colSpan={table.getAllColumns().length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}