migration

This commit is contained in:
Théo LAGACHE
2025-05-16 15:54:17 +02:00
parent 01019fe1a3
commit 6f2523e524
285 changed files with 15492 additions and 46090 deletions
@@ -1,35 +1,24 @@
"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 { 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";
import { EmailFormSchema, EmailFormType } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
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,
@@ -38,45 +27,40 @@ export const EmailForm = (props: EmailFormProps) => {
const mutation = useMutation({
mutationFn: async (values: EmailFormType) => {
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values})
const data = updateEmailSettings?.data?.data
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()
}
})
router.refresh();
},
});
return (
<TooltipProvider>
<Card>
<CardContent>
<Form form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
<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}) => (
render={({ field }) => (
<FormItem>
<FormLabel>From Email *</FormLabel>
<FormControl>
<Input
placeholder={"exemple@portabase.com"} {...field} />
<Input placeholder={"exemple@portabase.com"} {...field} />
</FormControl>
<FormDescription>{"The email from where the email will be send"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -84,16 +68,14 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpHost"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Server Host *</FormLabel>
<FormControl>
<Input
placeholder={"ssl0.ovh.net"} {...field} />
<Input placeholder={"ssl0.ovh.net"} {...field} />
</FormControl>
<FormDescription>{"Your email server host"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -101,16 +83,14 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpPort"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Server Port *</FormLabel>
<FormControl>
<Input
placeholder={"465"} {...field} />
<Input placeholder={"465"} {...field} />
</FormControl>
<FormDescription>{"Your email server port (send)"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -119,15 +99,15 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpPassword"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput placeholder="Password" {...field}/>
<PasswordInput placeholder="Password" {...field} />
</FormControl>
<FormDescription>{"Your email server password"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -135,29 +115,23 @@ export const EmailForm = (props: EmailFormProps) => {
control={form.control}
name="smtpUser"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>User Email *</FormLabel>
<FormControl>
<Input
placeholder={"exemple@portabase.com"} {...field} />
<Input placeholder={"exemple@portabase.com"} {...field} />
</FormControl>
<FormDescription>{"The email server user"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button>
Save
</Button>
<Button>Save</Button>
</div>
</Form>
</CardContent>
</Card>
</TooltipProvider>
)
}
);
};
@@ -1,31 +1,30 @@
"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";
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
import { setting as drizzleSetting } from "@/db/schema";
import { eq } from "drizzle-orm";
import { db } from "@/db";
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,
name: z.string(),
data: EmailFormSchema,
})
)
.action(async ({ parsedInput }) => {
const { name, data } = parsedInput;
const [updatedSettings] = await db
.update(drizzleSetting)
.set({
...data,
})
.where(eq(drizzleSetting.name, name))
.returning();
return {
data: updatedSettings,
}
})
};
});
@@ -1,4 +1,4 @@
import {z} from "zod";
import { z } from "zod";
export const EmailFormSchema = z.object({
smtpPassword: z.string(),
@@ -1,39 +1,40 @@
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/button-with-loading";
import {useMutation} from "@tanstack/react-query";
import {sendEmail} from "@/utils/email-helper";
import { EmailForm } from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/EmailForm";
import { Send } from "lucide-react";
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
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 { render } from "@react-email/render";
import { toast } from "sonner";
import { Setting } from "@/db/schema";
export type SettingsEmailTabProps = {
settings: Settings
}
settings: Setting;
};
export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
const mutation = useMutation({
mutationFn: async () => {
if (!props.settings.smtpUser || !props.settings.smtpFrom) {
toast.error("SMTP is not configured");
return;
}
const email = await sendEmail({
to: props.settings.smtpUser,
subject: "Portabase",
html: await render(TestEmailSettings(),{})
html: await render(TestEmailSettings(), {}),
from: props.settings.smtpFrom,
});
if(email.response){
if (email.response) {
toast.success("Test Email Successfully sent !");
}
}
})
},
});
const handleSendMailTest = async () => {
await mutation.mutateAsync()
}
await mutation.mutateAsync();
};
return (
<div className="flex flex-col h-full py-4">
@@ -43,18 +44,17 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
<ButtonWithLoading
isPending={mutation.isPending}
onClick={async () => {
await handleSendMailTest()
await handleSendMailTest();
}}
icon={<Send/>}
icon={<Send />}
text="Send email test"
size="default"
/>
)}
</div>
<div className="mt-5">
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings : null}/>
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings : null} />
</div>
</div>
)
}
);
};
@@ -1,78 +1,75 @@
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/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 {
updateStorageSettingsAction
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Info, 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 { 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 { updateStorageSettingsAction } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
import { Setting } from "@/db/schema";
export type SettingsStorageTabProps = {
settings: Settings
}
settings: Setting;
};
export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
const router = useRouter()
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 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"}}),
mutationFn: () => updateStorageSettingsAction({ name: "system", data: { storage: isSwitched ? "s3" : "local" } }),
onSuccess: () => {
toast.success(`Settings updated successfully.`);
router.refresh()
router.refresh();
},
onError: () => {
toast.error(`An error occurred while updating settings information.`);
},
});
const HandleSwitchStorage = async () => {
setIsSwitched(!isSwitched);
await updateMutation.mutateAsync()
}
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"/>
<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.
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"/>
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
<Switch
checked={isSwitched}
onCheckedChange={async () => {
await HandleSwitchStorage();
}}
id="storage-mode"
/>
</div>
<div>
<ButtonWithLoading
@@ -80,7 +77,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
disabled={!isSwitched}
isPending={mutation.isPending}
onClick={async () => {
await mutation.mutateAsync()
await mutation.mutateAsync();
}}
icon={<ShieldCheck />}
text="Test connexion"
@@ -89,10 +86,10 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
</div>
{isSwitched && (
<div className="mt-5">
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null}/>
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null} />
</div>
)}
</div>
</div>
)
}
);
};
@@ -1,28 +1,21 @@
"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 { 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";
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({
@@ -34,98 +27,92 @@ export const StorageS3Form = (props: S3FormProps) => {
const mutation = useMutation({
mutationFn: async (values: S3FormType) => {
console.log(values)
const updateS3Settings = await updateS3SettingsAction({name: "system", data: values})
const data = updateS3Settings?.data?.data
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()
}
})
router.refresh();
},
});
return (
<TooltipProvider>
<Card>
<CardContent>
<Form form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
<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}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint Url *</FormLabel>
<FormControl>
<Input
placeholder={"s3.eu-west-3.amazonaws.com"} {...field} />
<Input placeholder={"s3.eu-west-3.amazonaws.com"} {...field} />
</FormControl>
<FormDescription>{"Your s3 compatible url"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="s3AccessKeyId"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Access Key *</FormLabel>
<FormControl>
<Input
placeholder={"The access key token"} {...field} />
<Input placeholder={"The access key token"} {...field} />
</FormControl>
<FormDescription>{"Add your access key"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="s3SecretAccessKey"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Secret Key *</FormLabel>
<FormControl>
<Input
placeholder={"The secret key token"} {...field} />
<Input placeholder={"The secret key token"} {...field} />
</FormControl>
<FormDescription>{"Add your secret key"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="S3BucketName"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Bucket name *</FormLabel>
<FormControl>
<Input
placeholder={"my-bucket"} {...field} />
<Input placeholder={"my-bucket"} {...field} />
</FormControl>
<FormDescription>{"The bucket name where you want to store your data"}</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button>
Save
</Button>
<Button>Save</Button>
</div>
</Form>
</CardContent>
</Card>
</TooltipProvider>
)
}
);
};
@@ -1,51 +1,50 @@
"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";
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { db } from "@/db";
import { setting as drizzleSetting } from "@/db/schema";
import { S3FormSchema, StorageSwitchSchema } from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.schema";
import { eq } from "drizzle-orm";
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,
name: z.string(),
data: S3FormSchema,
})
)
.action(async ({ parsedInput }) => {
const { name, data } = parsedInput;
const [updatedSettings] = await db
.update(drizzleSetting)
.set({ ...data })
.where(eq(drizzleSetting.name, name))
.returning();
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,
name: z.string(),
data: StorageSwitchSchema,
})
)
.action(async ({ parsedInput }) => {
const { name, data } = parsedInput;
const [updatedSettings] = await db
.update(drizzleSetting)
.set({ ...data })
.where(eq(drizzleSetting.name, name))
.returning();
return {
data: updatedSettings,
}
})
};
});
@@ -1,4 +1,4 @@
import {z} from "zod";
import { z } from "zod";
export const S3FormSchema = z.object({
s3EndPointUrl: z.string(),
@@ -9,9 +9,8 @@ export const S3FormSchema = z.object({
export type S3FormType = z.infer<typeof S3FormSchema>;
export const StorageSwitchSchema = z.object({
storage: z.string(),
})
storage: z.enum(["local", "s3"]),
});
export type StorageType= z.infer<typeof StorageSwitchSchema>;
export type StorageType = z.infer<typeof StorageSwitchSchema>;
@@ -1,42 +1,43 @@
"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";
"use 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";
import { Setting } from "@/db/schema";
import { User } from "@/db/schema/01_user";
export type AdminTabsProps = {
currentUser: User;
users: User[];
settings: Settings;
}
settings: Setting;
};
export const AdminTabs = (props: AdminTabsProps) => {
const {currentUser, users, settings} = props;
const { 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>
<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}/>
<AdminUsersTable users={users} />
</TabsContent>
<TabsContent value="email">
<SettingsEmailTab settings={settings}/>
<SettingsEmailTab settings={settings} />
</TabsContent>
<TabsContent value="storage">
<SettingsStorageTab settings={settings}/>
<SettingsStorageTab settings={settings} />
</TabsContent>
</Tabs>
)
}
);
};
@@ -1,95 +1,21 @@
import {flexRender, Row, RowData} from "@tanstack/react-table";
import {User, UserOrganization} from "@prisma/client";
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
import {cn} from "@/lib/utils";
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/columns-users";
import { usersColumnsAdmin } from "@/components/wrappers/dashboard/admin/columns-users";
import { User } from "@/db/schema/01_user";
import { DataTable } from "../../common/table/data-table";
export type AdminUsersTableProps = {
currentUser: User;
users: User[]
}
users: User[];
};
export const AdminUsersTable = (props: AdminUsersTableProps) => {
const {currentUser, users} = props;
const { 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={usersColumnsAdmin}
data={users}
DataTable={UsersDataTableAdmin}
dataTableProps={{currentUser}}
/>
<DataTable columns={usersColumnsAdmin} data={users} />
</div>
</div>
)
}
export type usersDataTableProps = {
currentUser: User;
table: any,
}
export const UsersDataTableAdmin = ({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>) => {
return(
<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>
)
}
);
};
@@ -1,31 +1,28 @@
"use client"
"use client";
import {ColumnDef} from "@tanstack/react-table"
import {Badge} from "@/components/ui/badge";
import {User} from "@prisma/client";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {useState} from "react";
import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import { User } from "@prisma/client";
import { updateUserAction } from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Trash2 } from "lucide-react";
import { deleteUserAction } from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
export const usersColumnsAdmin: ColumnDef<User>[] = [
{
accessorKey: "role",
header: "Role",
cell: ({row}) => {
const router = useRouter();
const [role, setRole] = useState<string>(row.getValue("role"))
cell: ({ row }) => {
const [role, setRole] = useState<string>(row.getValue("role"));
const updateMutation = useMutation({
mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}),
mutationFn: () => updateUserAction({ id: row.original.id, data: { role: role } }),
onSuccess: () => {
toast.success(`User updated successfully.`);
// router.refresh()
},
onError: () => {
toast.error(`An error occurred while updating user information.`);
@@ -33,55 +30,52 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
});
const handleUpdateRole = async () => {
const nextRole = role === "admin" ? "pending"
: role === "pending" ? "user"
: "admin";
const nextRole = role === "admin" ? "pending" : role === "pending" ? "user" : "admin";
setRole(nextRole);
await updateMutation.mutateAsync()
await updateMutation.mutateAsync();
};
return <Badge
className="cursor-pointer"
onClick={() => handleUpdateRole()}
variant="outline">{role}</Badge>
return (
<Badge className="cursor-pointer" onClick={() => handleUpdateRole()} variant="outline">
{role}
</Badge>
);
},
},
{
accessorKey: "name",
header: "Name"
header: "Name",
},
{
accessorKey: "email",
header: "Email"
header: "Email",
},
{
accessorKey: "updatedAt",
header: "Updated At",
cell: ({row}) => {
cell: ({ row }) => {
return new Date(row.getValue("updatedAt")).toLocaleString("fr-FR");
},
},
{
accessorKey: "authMethod",
header: "Method",
cell: ({row}) => {
return <Badge variant="outline">{row.getValue("authMethod")}</Badge>
cell: ({ row }) => {
return <Badge variant="outline">{row.getValue("authMethod")}</Badge>;
},
},
{
header: "Action",
id: "actions",
cell: ({ row, table }) => {
cell: ({ row }) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: () => deleteUserAction(row.original.id),
onSuccess: async () => {
toast.success('User deleted successfully.');
router.refresh()
toast.success("User deleted successfully.");
router.refresh();
},
})
});
return (
<div className="flex items-center gap-2">
@@ -94,9 +88,8 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
}}
size="icon"
/>
</div>
);
},
},
]
];
@@ -1,34 +1,29 @@
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import Link from "next/link";
import {ValueIcon} from "@radix-ui/react-icons";
import {Circle} from "lucide-react";
import {formatDateLastContact} from "@/utils/date-formatting";
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
import { formatDateLastContact } from "@/utils/date-formatting";
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
import { Agent } from "@/db/schema";
export type agentCardProps = {
data: any
}
data: Agent;
};
export const AgentCard = (props: agentCardProps) => {
const {data: agent} = props;
const { data: agent } = props;
return (
<Link href={`/dashboard/agents/${agent.id}`}>
<Card className="flex flex-row justify-between">
<div className="">
<CardHeader>{agent.name}</CardHeader>
<CardContent>
Last contact : {formatDateLastContact(agent.lastContact)}
</CardContent>
<CardContent>Last contact : {formatDateLastContact(agent.lastContact)}</CardContent>
</div>
<div className="mt-3 mr-3">
<ConnectionCircle date={agent.lastContact}/>
<ConnectionCircle date={agent.lastContact} />
</div>
</Card>
</Link>
)
}
);
};
@@ -1,25 +1,28 @@
"use client"
import {generateEdgeKey} from "@/utils/edge_key";
import {getServerUrl} from "@/utils/get-server-url";
import {Agent} from "@prisma/client";
import {PasswordInput} from "@/components/wrappers/auth/PaswordInput/password-input";
import {useState} from "react";
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
"use client";
import { generateEdgeKey } from "@/utils/edge_key";
import { getServerUrl } from "@/utils/get-server-url";
import { PasswordInput } from "@/components/wrappers/auth/PaswordInput/password-input";
import { useState } from "react";
import { CopyButton } from "@/components/wrappers/common/button/copy-button";
import { Agent } from "@/db/schema";
export type AgentCardKeyProps = {
agent: Agent
}
agent: Agent;
};
export const AgentCardKey = (props: AgentCardKeyProps) => {
const edge_key = generateEdgeKey(getServerUrl(), props.agent.id);
const [code, setCode] = useState<string>(`${edge_key}`);
return(
return (
<>
<PasswordInput value={code} onChange={(value: string) => {setCode(edge_key)} }/>
<CopyButton className="mt-5" value={code}/>
<PasswordInput
value={code}
onChange={() => {
setCode(edge_key);
}}
/>
<CopyButton className="mt-5" value={code} />
</>
)
}
);
};
@@ -1,28 +1,24 @@
"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 {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import {AgentSchema, AgentType} from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
import {toast} from "sonner";
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.action";
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 { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { AgentSchema, AgentType } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
import { toast } from "sonner";
import { createAgentAction, updateAgentAction } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.action";
export type agentFormProps = {
defaultValues?: AgentType;
agentId?: string;
}
};
export const AgentForm = (props: agentFormProps) => {
const isCreate = !Boolean(props.defaultValues)
// const defaultValues = isCreate ? {slug: ""} : props.defaultValues
const isCreate = !Boolean(props.defaultValues);
const form = useZodForm({
schema: AgentSchema,
@@ -33,14 +29,16 @@ export const AgentForm = (props: agentFormProps) => {
const mutation = useMutation({
mutationFn: async (values: AgentType) => {
console.log("values", values)
console.log("values", values);
const createAgent = isCreate ? await createAgentAction(values) : await updateAgentAction({
id: props.agentId ?? "-",
data: values
});
const createAgent = isCreate
? await createAgentAction(values)
: await updateAgentAction({
id: props.agentId ?? "-",
data: values,
});
const data = createAgent?.data?.data
const data = createAgent?.data?.data;
if (createAgent?.serverError || !data) {
console.log(createAgent?.serverError);
toast.error(createAgent?.serverError);
@@ -48,83 +46,78 @@ export const AgentForm = (props: agentFormProps) => {
}
toast.success(`Success`);
router.push(`/dashboard/agents/${data.id}`);
router.refresh()
}
})
router.refresh();
},
});
return (
<TooltipProvider>
<Card>
<CardContent>
<Form form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
<Form
form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Agent 1" {...field} />
<Input placeholder="Agent 1" {...field} />
</FormControl>
<FormDescription>Your agent project name</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
defaultValue=""
name="slug"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
value={field.value ?? ""}
placeholder="agent-1" {...field}
//value={field.value ?? ""}
placeholder="agent-1"
{...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
field.onChange(value)
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
field.onChange(value);
}}
/>
</FormControl>
<FormDescription>The slug is used in the url of the agent</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
defaultValue=""
name="description"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input
placeholder='This agent is for the client exemple.com' {...field}
value={field.value ?? ""}/>
<Input placeholder="This agent is for the client exemple.com" {...field} value={field.value ?? ""} />
</FormControl>
<FormDescription>Enter your project agent description</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<Button>
{isCreate ? `Create agent` : `Save agent`}
</Button>
<Button>{isCreate ? `Create agent` : `Save agent`}</Button>
</Form>
</CardContent>
</Card>
</TooltipProvider>
)
}
);
};
@@ -1,68 +1,44 @@
"use server"
import {ActionError, userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {AgentSchema} from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
import {z} from "zod";
"use server";
import { ActionError, userAction } from "@/safe-actions";
import { AgentSchema } from "@/components/wrappers/dashboard/agent/AgentForm/agent-form.schema";
import { z } from "zod";
import { eq, and, ne, count } from "drizzle-orm";
import { db } from "@/db";
import { agent } from "@/db/schema";
const verifySlugUniqueness = async (slug: string, agentId?: string) => {
const slugExists = await prisma.agent.count({
where: {
slug: slug,
id: agentId ? {
not: agentId
} : undefined,
},
})
const conditions = agentId ? and(eq(agent.slug, slug), ne(agent.id, agentId)) : eq(agent.slug, slug);
console.log(slugExists)
if (slugExists) {
const [countResult] = await db.select({ count: count() }).from(agent).where(conditions);
if (countResult.count > 0) {
throw new ActionError("Slug already exists");
}
}
};
export const createAgentAction = userAction.schema(AgentSchema).action(async ({ parsedInput }) => {
await verifySlugUniqueness(parsedInput.slug);
export const createAgentAction = userAction
.schema(AgentSchema)
.action(async ({parsedInput, ctx}) => {
// Verify if slug already exist
await verifySlugUniqueness(parsedInput.slug);
const agent = await prisma.agent.create({
data: {
...parsedInput
}
})
// await sendEmailIfUserCreatedFirstForm(ctx.user)
return {
data: agent,
}
});
const [createdAgent] = await db.insert(agent).values(parsedInput).returning();
return {
data: createdAgent,
};
});
export const updateAgentAction = userAction
.schema(
z.object({
id: z.string(),
data: AgentSchema,
}
)
id: z.string(),
data: AgentSchema,
})
)
.action(async ({parsedInput, ctx}) => {
.action(async ({ parsedInput }) => {
await verifySlugUniqueness(parsedInput.data.slug, parsedInput.id);
console.log("parsedInput", parsedInput.data)
const updatedAgent = await prisma.agent.update({
where: {
id: parsedInput.id,
},
data: parsedInput.data,
})
const [updatedAgent] = await db.update(agent).set(parsedInput.data).where(eq(agent.id, parsedInput.id)).returning();
return {
data: updatedAgent,
}
})
};
});
@@ -1,9 +1,13 @@
import {z} from "zod";
import { z } from "zod";
export const AgentSchema = z.object({
name: z.string(),
slug: z.string().regex(/^[a-zA-Z0-9_-]*$/).min(5).max(25),
description: z.string().optional().nullable(),
slug: z
.string()
.regex(/^[a-zA-Z0-9_-]*$/)
.min(5)
.max(25),
description: z.string(),
});
export type AgentType = z.infer<typeof AgentSchema>;
@@ -1,39 +1,26 @@
"use client"
"use client";
import {Button} from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import {generateEdgeKey} from "@/utils/edge_key";
import {Copy} from "lucide-react";
import {PropsWithChildren, useState} from "react";
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
import {Agent} from "@prisma/client";
import {getServerUrl} from "@/utils/get-server-url";
import {CodeSnippet} from "@/components/wrappers/codeSnippet/CodeSnippet";
import { generateEdgeKey } from "@/utils/edge_key";
import { PropsWithChildren } from "react";
import { CopyButton } from "@/components/wrappers/common/button/copy-button";
import { getServerUrl } from "@/utils/get-server-url";
import { CodeSnippet } from "@/components/wrappers/code-snippet/CodeSnippet";
import { Agent } from "@/db/schema";
export type agentRegistrationDialogProps = PropsWithChildren<{
agent: Agent
}>
agent: Agent;
}>;
export function AgentModalKey(props: agentRegistrationDialogProps) {
const edge_key = generateEdgeKey(getServerUrl(), props.agent.id);
const code = `EDGE_KEY = ${edge_key}`;
return (
<Dialog>
<DialogTrigger asChild>
{props.children}
</DialogTrigger>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent className="sm:max-w-[425px] w-full">
<DialogHeader>
<DialogTitle>Agent Edge Key</DialogTitle>
@@ -46,11 +33,11 @@ export function AgentModalKey(props: agentRegistrationDialogProps) {
</div>
<DialogFooter>
<div className="flex items-center justify-between w-full">
<CopyButton value={code}/>
<CopyButton value={code} />
<Button type="submit">Save changes</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
}
@@ -1,43 +1,41 @@
"use server"
"use server";
import {z} from "zod";
import { z } from "zod";
import { userAction } from "@/safe-actions";
import { db } from "@/db";
import { backup } from "@/db/schema";
import { ServerActionResult } from "@/types/action-type";
import { Backup } from "@/db/schema";
import {userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {ServerActionResult} from "@/types/action-type";
import {Backup} from "@prisma/client";
export const backupButtonAction = userAction.schema(z.string()).action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
try {
const [createdBackup] = await db
.insert(backup)
.values({
databaseId: parsedInput,
status: "waiting",
})
.returning();
return {
success: true,
value: createdBackup,
actionSuccess: {
message: "Backup has been successfully created.",
messageParams: { databaseId: parsedInput },
},
};
} catch (error) {
console.error("Error creating backup:", error);
export const backupButtonAction = userAction
.schema(z.string())
.action(async ({ parsedInput, ctx }): Promise<ServerActionResult<Backup>> => {
try {
const backup = await prisma.backup.create({
data: {
databaseId: parsedInput,
status: "waiting",
},
});
return {
success: true,
value: backup,
actionSuccess: {
message: "Backup has been successfully created.",
messageParams: { databaseId: parsedInput },
},
};
} catch (error) {
console.error("Error creating backup:", error);
return {
success: false,
actionError: {
message: "Failed to create backup.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: { databaseId: parsedInput },
},
};
}
});
return {
success: false,
actionError: {
message: "Failed to create backup.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: { databaseId: parsedInput },
},
};
}
});
@@ -1,35 +1,34 @@
"use client"
"use client";
import {useRouter} from "next/navigation";
import { useRouter } from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import { useMutation } from "@tanstack/react-query";
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 { backupButtonAction } from "@/components/wrappers/dashboard/backup/backup-button/backup-button.action";
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
export type BackupButtonProps = {
databaseId: string
disable: boolean
}
databaseId: string;
disable: boolean;
};
export const BackupButton = (props: BackupButtonProps) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: async (databaseId: string) => {
const backup = await backupButtonAction(databaseId)
console.log(backup)
if (backup.data.success) {
const backup = await backupButtonAction(databaseId);
if (backup?.data?.success) {
toast.success(backup.data.actionSuccess?.message || "Backup created successfully!");
router.refresh()
router.refresh();
} else {
toast.error(backup.serverError || "Failed to create backup.");
toast.error(backup?.serverError || "Failed to create backup.");
}
}
})
},
});
const HandleAction = async () => {
await mutation.mutateAsync(props.databaseId)
}
await mutation.mutateAsync(props.databaseId);
};
return (
<ButtonWithLoading
@@ -38,8 +37,8 @@ export const BackupButton = (props: BackupButtonProps) => {
isPending={mutation.isPending}
size={"default"}
onClick={async () => {
await HandleAction()
await HandleAction();
}}
/>
)
}
);
};
@@ -2,17 +2,17 @@ import { useState } from "react";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import {isValidCronPart} from "@/utils/cron";
import { isValidCronPart } from "@/utils/cron";
export const AdvancedCronSelect = ({
id,
label,
options,
type,
value,
defaultValue,
onValueChange,
}: {
id,
label,
options,
type,
value,
defaultValue,
onValueChange,
}: {
id: string;
label: string;
options: string[];
@@ -38,7 +38,9 @@ export const AdvancedCronSelect = ({
return (
<div className="grid grid-cols-2 items-center gap-2">
<Label htmlFor={id} className="text-left">{label}</Label>
<Label htmlFor={id} className="text-left">
{label}
</Label>
{!isAdvanced ? (
<Select
id={id}
@@ -1,95 +1,71 @@
"use client"
import {Clock9} from "lucide-react";
import {Button} from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription, DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog";
import {CronInput} from "@/components/wrappers/dashboard/database/CronButton/CronInput";
import {Switch} from "@/components/ui/switch";
import {Label} from "@/components/ui/label"
import {Separator} from "@/components/ui/separator";
import {useState} from "react";
import {useMutation} from "@tanstack/react-query";
import {
updateStorageSettingsAction
} from "@/components/wrappers/dashboard/admin/AdminStorageTab/StorageS3Form/s3-form.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {Database} from "@prisma/client";
import {
updateBackupPolicyAction,
updateDatabaseBackupPolicyAction
} from "@/components/wrappers/dashboard/database/CronButton/cron.action";
"use client";
import { Clock9 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { CronInput } from "@/components/wrappers/dashboard/database/CronButton/CronInput";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/CronButton/cron.action";
import { Database } from "@/db/schema";
export type CronButtonProps = {
database: Database
}
database: Database;
};
export const CronButton = (props: CronButtonProps) => {
const router = useRouter();
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
const updateDatabaseBackupPolicy = useMutation({
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: props.database.id, backupPolicy:value}),
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
onSuccess: () => {
toast.success(`Method updated successfully.`);
router.refresh()
router.refresh();
},
onError: () => {
toast.error(`An error occurred while updating backup method.`);
},
});
const handleTypeChange = async (state: boolean) => {
setIsSwitched(state);
if(state == false) {
await updateDatabaseBackupPolicy.mutateAsync("")
if (state == false) {
await updateDatabaseBackupPolicy.mutateAsync("");
}
}
};
return (
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
{...props}
>
<Clock9/>
<Button variant="outline" {...props}>
<Clock9 />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Backup method</DialogTitle>
<DialogDescription>
Your settings for the backup method
</DialogDescription>
<DialogDescription>Your settings for the backup method</DialogDescription>
</DialogHeader>
<Separator/>
<Separator />
<h1>
Select your backup method
</h1>
<h1>Select your backup method</h1>
<div className="flex items-center space-x-2">
<Label>Manual / Automatic </Label>
<Switch
checked={isSwitched}
onCheckedChange={async () => {
await handleTypeChange(!isSwitched)
await handleTypeChange(!isSwitched);
}}
id="type-mode"/>
id="type-mode"
/>
</div>
{isSwitched ?
<CronInput database={props.database}/>
:null}
{isSwitched ? <CronInput database={props.database} /> : null}
</DialogContent>
</Dialog>
)
}
);
};
@@ -1,20 +1,16 @@
import { AdvancedCronSelect } from "./AdvancedCronSelect";
import {
updateBackupPolicyAction,
updateDatabaseBackupPolicyAction
} from "@/components/wrappers/dashboard/database/CronButton/cron.action";
import {useMutation} from "@tanstack/react-query";
import {useState} from "react";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
import {Database} from "@prisma/client";
import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/CronButton/cron.action";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Database } from "@/db/schema";
export type CronInputProps = {
database : Database
}
database: Database;
};
export const CronInput = ({ database }: CronInputProps) => {
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
@@ -31,7 +27,7 @@ export const CronInput = ({ database }: CronInputProps) => {
},
});
const handleChangeCron = (type: string, value: string) => {
const handleChangeCron = (type: "minute" | "hour" | "day-of-month" | "month" | "day-of-week", value: string) => {
const cronParts = cron.split(" ");
const indexMap = { minute: 0, hour: 1, "day-of-month": 2, month: 3, "day-of-week": 4 };
cronParts[indexMap[type]] = value;
@@ -96,9 +92,7 @@ export const CronInput = ({ database }: CronInputProps) => {
<div className="font-semibold">Cron Expression</div>
<div className="font-mono text-muted-foreground">{cron}</div>
</div>
<div className="text-sm text-muted-foreground">
This cron expression determines when the job will run.
</div>
<div className="text-sm text-muted-foreground">This cron expression determines when the job will run.</div>
</div>
<div className="flex justify-between gap-2">
<Button
@@ -120,4 +114,4 @@ export const CronInput = ({ database }: CronInputProps) => {
</div>
</>
);
};
};
@@ -1,31 +1,31 @@
"use server"
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {prisma} from "@/prisma";
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { db } from "@/db";
import { database } from "@/db/schema";
import { eq } from "drizzle-orm";
export const updateDatabaseBackupPolicyAction = userAction
.schema(
z.object({
databaseId: z.string(),
backupPolicy: z.string(),
}
)
)
.action(async ({parsedInput, ctx}) => {
const cronPolicy = parsedInput.backupPolicy == "" ? null : parsedInput.backupPolicy
const updatedDatabase = await prisma.database.update({
where: {
id: parsedInput.databaseId,
},
data: {
backupPolicy: cronPolicy,
}
databaseId: z.string(),
backupPolicy: z.string(),
})
)
.action(async ({ parsedInput }) => {
const cronPolicy = parsedInput.backupPolicy === "" ? null : parsedInput.backupPolicy;
const [updated] = await db
.update(database)
.set({
backupPolicy: cronPolicy,
})
.where(eq(database.id, parsedInput.databaseId))
.returning()
.execute();
return {
data: updatedDatabase,
}
})
data: updated,
};
});
@@ -1,119 +1,113 @@
"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 {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import {DatabaseSchema, DatabaseType} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
import {updateDatabaseAction} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.action";
import {toast} from "sonner";
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 { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { DatabaseSchema, DatabaseType } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
import { updateDatabaseAction } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.action";
import { toast } from "sonner";
export type DatabaseFormProps = {
defaultValues?: DatabaseType;
databaseId?: string;
}
};
export const DatabaseForm = (props: DatabaseFormProps) => {
const { defaultValues, databaseId } = props;
const {defaultValues, databaseId} = props;
const isCreate = !Boolean(defaultValues)
const isCreate = !Boolean(defaultValues);
const form = useZodForm({
schema: DatabaseSchema,
defaultValues: {...defaultValues},
defaultValues: { ...defaultValues },
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: DatabaseType) => {
if (!databaseId) {
throw new Error("Database ID is required");
}
const database = await updateDatabaseAction({ id: databaseId, data: values });
const database = await updateDatabaseAction({id: databaseId, data: values});
if (!database) {
toast.error("Failed to update database");
return;
}
if (database.serverError) {
console.error(database?.serverError);
toast.error(database?.serverError);
toast.error(database.serverError);
return;
}
console.log(database)
toast.success(`Database settings successfully updated!`);
router.back()
}
})
router.back();
},
});
return (
<TooltipProvider>
<Card>
<CardContent>
<Form form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
console.log("sssssss")
await mutation.mutateAsync(values);
}}
<Form
form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
console.log("sssssss");
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input disabled placeholder="Database 1" {...field}/>
<Input disabled placeholder="Database 1" {...field} />
</FormControl>
<FormDescription>Your database project name setup in agent</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dbms"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Database type</FormLabel>
<FormControl>
<Input disabled placeholder="PostgreSQL" {...field}/>
<Input disabled placeholder="PostgreSQL" {...field} />
</FormControl>
<FormDescription>Your database project name setup in agent</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Prod database for project 1" {...field}/>
<Input placeholder="Prod database for project 1" {...field} />
</FormControl>
<FormDescription>Add a short description about this database</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<Button>
{isCreate ? `Create database` : `Save database`}
</Button>
<Button>{isCreate ? `Create database` : `Save database`}</Button>
</Form>
</CardContent>
</Card>
</TooltipProvider>
)
}
);
};
@@ -1,25 +1,21 @@
"use server"
import {z} from "zod";
import {userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {DatabaseSchema} from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
"use server";
import { z } from "zod";
import { userAction } from "@/safe-actions";
import { db } from "@/db";
import { database } from "@/db/schema";
import { eq } from "drizzle-orm";
import { DatabaseSchema } from "@/components/wrappers/dashboard/database/DatabaseForm/form-database.schema";
export const updateDatabaseAction = userAction
.schema(
z.object({
id: z.string(),
data: DatabaseSchema,
}
)
id: z.string(),
data: DatabaseSchema,
})
)
.action(async ({parsedInput, ctx}) => {
return prisma.database.update({
where: {
id: parsedInput.id,
},
data: parsedInput.data,
});
})
.action(async ({ parsedInput }) => {
const [updated] = await db.update(database).set(parsedInput.data).where(eq(database.id, parsedInput.id)).returning().execute();
return updated;
});
@@ -1,12 +1,9 @@
import {z} from "zod";
const cronRegex = /^(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,2}|\*|(\d{1,2}-\d{1,2})|(\d{1,2}\/\d{1,2}))\s+(\d{1,7}|\*|(\d{1,7}-\d{1,7})|(\d{1,7}\/\d{1,7}))$/;
import { z } from "zod";
export const DatabaseSchema = z.object({
name: z.string().readonly(),
description: z.string().optional(),
dbms: z.string().readonly(),
dbms: z.enum(["active", "inactive"]).readonly(),
});
export type DatabaseType = z.infer<typeof DatabaseSchema>;
@@ -1,20 +1,17 @@
"use client"
import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
"use client";
import { buttonVariants } from "@/components/ui/button";
import { GearIcon } from "@radix-ui/react-icons";
import Link from "next/link";
import {usePathname} from "next/navigation";
import { usePathname } from "next/navigation";
export type EditButtonProps = {}
export type EditButtonProps = {};
export const EditButton = (props: EditButtonProps) => {
const pathname = usePathname();
return(
<Link
className={buttonVariants({ variant: "outline" })}
href={`${pathname}/edit`}
>
return (
<Link className={buttonVariants({ variant: "outline" })} href={`${pathname}/edit`}>
<GearIcon className="w-7 h-7" />
</Link>
)
}
);
};
@@ -1,38 +1,24 @@
"use client";
import {useState} from "react";
import { useState } from "react";
import {DateTimePicker} from "@/components/wrappers/common/daytime-picker";
import {Button} from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {RestoreSchema} from "@/components/wrappers/dashboard/database/restore-form.schema";
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
import {useMutation} from "@tanstack/react-query";
import {ComboBox, ComboBoxFormItem} from "@/components/wrappers/common/combobox";
import {Label} from "@/components/ui/label";
import { DateTimePicker } from "@/components/wrappers/common/day-time-picker";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RestoreSchema } from "@/components/wrappers/dashboard/database/restore-form.schema";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ComboBox, ComboBoxFormItem } from "@/components/wrappers/common/combobox";
import { Label } from "@/components/ui/label";
export type restoreFormProps = {
databaseToRestore: any
databases: any[]
backups: any[]
}
databaseToRestore: any;
databases: any[];
backups: any[];
};
export const RestoreForm = (props: restoreFormProps) => {
const {databaseToRestore, databases, backups} = props
console.log("bacups", backups)
console.log("bacups", databaseToRestore)
const { databaseToRestore, databases, backups } = props;
const backupLocations = [
{
@@ -43,7 +29,7 @@ export const RestoreForm = (props: restoreFormProps) => {
value: "desktop-file",
label: "Desktop File",
},
]
];
const executionModes = [
{
@@ -54,29 +40,23 @@ export const RestoreForm = (props: restoreFormProps) => {
value: "scheduled",
label: "Scheduled",
},
]
];
const [selectedDatabaseId, setSelectedDatabaseId] = useState(databaseToRestore.id)
const [selectedDatabaseId, setSelectedDatabaseId] = useState(databaseToRestore.id);
const filteredBackups = backups.filter(backup => backup.databaseId == selectedDatabaseId)
const filteredBackups = backups.filter((backup) => backup.databaseId == selectedDatabaseId);
const defaultValues = {
executionMode: "immediate",
backupLocation: "remote-file",
}
executionMode: "immediate" as const,
backupLocation: "remote-file" as const,
};
const form = useZodForm({
schema: RestoreSchema,
defaultValues: defaultValues
defaultValues: defaultValues,
});
const values = form.getValues()
console.log("values", values)
const mutation = useMutation({})
const values = form.getValues();
return (
<div>
@@ -89,36 +69,34 @@ export const RestoreForm = (props: restoreFormProps) => {
<FormField
control={form.control}
name="backupLocation"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Backup location</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue/>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{backupLocations.map((backupLocation, key) =>
{backupLocations.map((backupLocation, key) => (
<SelectItem key={key} value={backupLocation.value}>
{backupLocation.label}
</SelectItem>
)}
))}
</SelectContent>
</Select>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
{values.backupLocation == "remote-file" ?
{values.backupLocation == "remote-file" ? (
<>
<div className="flex flex-col">
<Label>Database</Label>
<ComboBox
values={databases.map(database =>
({"value": database.id, "label": database.name})
)}
values={databases.map((database) => ({ value: database.id, label: database.name }))}
onValueChange={setSelectedDatabaseId}
defaultValue={selectedDatabaseId}
searchField
@@ -127,81 +105,82 @@ export const RestoreForm = (props: restoreFormProps) => {
<FormField
control={form.control}
name="remoteBackup"
render={({field}) => (
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Remote backup</FormLabel>
<ComboBoxFormItem
values={filteredBackups.map(backup => ({
"value": backup.id,
"label": backup.createdAt.toString()
})
)}
values={filteredBackups.map((backup) => ({
value: backup.id,
label: backup.createdAt.toString(),
}))}
{...field}
/>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
</>
: null}
) : null}
{values.backupLocation == "desktop-file" ?
{values.backupLocation == "desktop-file" ? (
<FormField
control={form.control}
name="uploadedBackupFile"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>File</FormLabel>
<FormControl>
<Input id="uploadedBackupFile" type="file" {...field}/>
<Input id="uploadedBackupFile" type="file" {...field} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/> : null}
/>
) : null}
<FormField
control={form.control}
name="executionMode"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Execution mode</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue/>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{executionModes.map((executionMode, key) =>
{executionModes.map((executionMode, key) => (
<SelectItem key={key} value={executionMode.value}>
{executionMode.label}
</SelectItem>
)}
))}
</SelectContent>
</Select>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
{values.executionMode == "scheduled" ?
{values.executionMode == "scheduled" ? (
<FormField
control={form.control}
name="scheduledDatetime"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Scheduled date</FormLabel>
<FormControl>
<DateTimePicker value={field.value} onChange={field.onChange}/>
<DateTimePicker value={field.value} onSelect={field.onChange} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/> : null}
/>
) : null}
<Button type="submit">Launch restore</Button>
</Form>
</div>
)
}
);
};
@@ -1,24 +1,39 @@
import {z} from "zod";
import { z } from "zod";
const ImmediateExecutionSchema = z.object({
executionMode: z.literal('immediate'),
executionMode: z.literal("immediate"),
});
const ScheduledExecutionSchema = z.object({
executionMode: z.literal('scheduled'),
executionMode: z.literal("scheduled"),
scheduledDatetime: z.date(),
});
const CommonBackupSchema = z.union([ImmediateExecutionSchema, ScheduledExecutionSchema]);
const RemoteImmediateSchema = z
.object({
backupLocation: z.literal("remote-file"),
})
.merge(ImmediateExecutionSchema);
const RemoteBackupSchema = z.object({
backupLocation: z.literal('remote-file'),
}).merge(CommonBackupSchema);
const RemoteScheduledSchema = z
.object({
backupLocation: z.literal("remote-file"),
})
.merge(ScheduledExecutionSchema);
const DesktopBackupSchema = z.object({
backupLocation: z.literal('desktop-file'),
uploadedBackupFile: z.instanceof(File),
}).merge(CommonBackupSchema);
const DesktopImmediateSchema = z
.object({
backupLocation: z.literal("desktop-file"),
uploadedBackupFile: z.instanceof(File),
})
.merge(ImmediateExecutionSchema);
export const RestoreSchema = z.union([RemoteBackupSchema, DesktopBackupSchema]);
const DesktopScheduledSchema = z
.object({
backupLocation: z.literal("desktop-file"),
uploadedBackupFile: z.instanceof(File),
})
.merge(ScheduledExecutionSchema);
export const RestoreSchema = z.union([RemoteImmediateSchema, RemoteScheduledSchema, DesktopImmediateSchema, DesktopScheduledSchema]);
export type RestoreType = z.infer<typeof RestoreSchema>;
@@ -1,30 +1,34 @@
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"
import {currentUser} from "@/auth/current-user";
import {SidebarMenuButton} from "@/components/ui/sidebar";
import {ChevronUp} from "lucide-react";
import {LoggedInDropdown} from "@/components/wrappers/dashboard/loggedInDropdown/LoggedInDropdown";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { SidebarMenuButton } from "@/components/ui/sidebar";
import { ChevronUp } from "lucide-react";
import { LoggedInDropdown } from "@/components/wrappers/dashboard/loggedInDropdown/LoggedInDropdown";
import { currentUser } from "@/lib/auth/current-user";
export const LoggedInButton = async () => {
const user = await currentUser();
const user = await currentUser()
// if (!user) {
// return <SignInButton/>
// }
if (!user) return null;
return (
<LoggedInDropdown user={user}>
<LoggedInDropdown
user={{
...user,
image: user.image ?? null,
role: user.role ?? null,
banned: user.banned ?? null,
banReason: user.banReason ?? null,
banExpires: user.banExpires ?? null,
deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
}}
>
<SidebarMenuButton>
<Avatar className="size-6">
<AvatarFallback>{user.name?.[0]}</AvatarFallback>
{user.image ? (
<AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`}/>
) : null}
<AvatarFallback>{user.name[0].toUpperCase()}</AvatarFallback>
{user.image ? <AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`} /> : null}
</Avatar>
<span>{user.name}</span>
<ChevronUp className="ml-auto"/>
<span className="first-letter:capitalize">{user.name}</span>
<ChevronUp className="ml-auto" />
</SidebarMenuButton>
</LoggedInDropdown>
)
}
);
};
@@ -1,55 +1,63 @@
"use client"
"use client";
import {PropsWithChildren} from "react";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {signOutAction} from "@/features/auth/auth.action";
import {redirect} from "next/navigation";
import {CircleUser, LogOut, ShieldHalf} from "lucide-react";
import {User} from "@prisma/client";
import { PropsWithChildren } from "react";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { redirect } from "next/navigation";
import { CircleUser, LogOut, ShieldHalf } from "lucide-react";
import { signOut } from "@/lib/auth/auth-client";
import { useRouter } from "next/navigation";
import { User } from "@/db/schema/01_user";
export type LoggedInDropdownProps = PropsWithChildren<{
user: User
}>
user: User;
}>;
export const LoggedInDropdown = (props: LoggedInDropdownProps) => {
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{props.children}
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
className="w-[--radix-popper-anchor-width]"
>
<DropdownMenuItem onClick={() => {
redirect("/dashboard/profile")
}}>
<DropdownMenuTrigger asChild>{props.children}</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="w-[--radix-popper-anchor-width]">
<DropdownMenuItem
onClick={() => {
redirect("/dashboard/profile");
}}
>
<div className="flex justify-start items-center gap-2">
<CircleUser size={16}/>
<CircleUser size={16} />
<span>Account</span>
</div>
</DropdownMenuItem>
{props.user.role == "admin" ?
<DropdownMenuItem onClick={() => {
redirect("/dashboard/admin")
}}>
{(props.user.role === "superadmin" || props.user.role === "admin") && (
<DropdownMenuItem
onClick={() => {
redirect("/dashboard/admin");
}}
>
<div className="flex justify-start items-center gap-2">
<ShieldHalf size={16}/>
<ShieldHalf size={16} />
<span>Administration Panel</span>
</div>
</DropdownMenuItem>
: null}
<DropdownMenuItem onClick={() => {
signOutAction()
}}>
)}
<DropdownMenuItem
onClick={async () => {
await signOut({
fetchOptions: {
onSuccess: () => {
router.push("/login");
},
},
});
}}
>
<div className="flex justify-start items-center gap-2">
<LogOut size={16}/>
<LogOut size={16} />
<span>Log out</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
);
};
@@ -1,42 +1,38 @@
"use client"
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {
deleteOrganizationAction,
} from "@/components/wrappers/dashboard/organization/organization.action";
import {useMutation} from "@tanstack/react-query";
import {setCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
"use client";
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
import { deleteOrganizationAction } from "@/components/wrappers/dashboard/organization/organization.action";
import { useMutation } from "@tanstack/react-query";
import { setCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
export type DeleteOrganizationButtonProps = {
organizationSlug: string;
}
};
export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: () => deleteOrganizationAction(props.organizationSlug),
onSuccess: async (result) => {
console.log(result);
if(result.data.success) {
await setCurrentOrganizationSlug("default")
router.push("/")
if (result.data.success) {
await setCurrentOrganizationSlug("default");
router.push("/");
toast.success(result.data.actionSuccess.message);
}else{
} else {
toast.error(result.data.actionError.message);
}
},
})
});
return(
return (
<ButtonWithConfirm
onClick={() => {
mutation.mutate()
mutation.mutate();
}}
isPending={mutation.isPending}
text="Delete Organization"
variant="destructive"/>
)
}
variant="destructive"
/>
);
};
@@ -1,111 +1,86 @@
"use client";
import {Card, CardContent, CardHeader} 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 {useRouter} from "next/navigation";
import {Organization, User} from "@prisma/client"
import {MultiSelect} from "@/components/wrappers/common/multiSelect/MultiSelect";
import {
OrganizationFormSchema,
OrganizationFormType
} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import {
createOrganizationAction,
updateOrganizationAction
} from "@/components/wrappers/dashboard/organization/organization.action";
import {toast} from "sonner";
import { Card, CardContent, CardHeader } 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 { useRouter } from "next/navigation";
import { Organization, User } from "@prisma/client";
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
import { OrganizationFormSchema, OrganizationFormType } from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import { createOrganizationAction, updateOrganizationAction } from "@/components/wrappers/dashboard/organization/organization.action";
import { toast } from "sonner";
export type organizationFormProps = {
defaultValues?: Organization;
users: User[]
}
users: User[];
};
export const OrganizationForm = (props: organizationFormProps) => {
const router = useRouter();
const isCreate = !Boolean(props.defaultValues)
const isCreate = !Boolean(props.defaultValues);
const formatUsersList = (users: User[]) => {
return users.map(user => ({
return users.map((user) => ({
value: user.id,
label: `${user.name} | ${user.email}`,
}));
};
const formatDefaultUsers = (users: OrganizationFormType['users']): string[] => {
console.log(users)
return users.map(user => user.userId );
const formatDefaultUsers = (users: OrganizationFormType["users"]): string[] => {
console.log(users);
return users.map((user) => user.userId);
};
const formattedDefaultValues = {
...props.defaultValues,
users: !isCreate ? formatDefaultUsers(props.defaultValues?.users) : []
}
users: !isCreate ? formatDefaultUsers(props.defaultValues?.users) : [],
};
const form = useZodForm({
schema: OrganizationFormSchema,
defaultValues: formattedDefaultValues,
});
const mutation = useMutation({
mutationFn: async (values: OrganizationFormType) => {
console.log(values)
const organization = await updateOrganizationAction({data: values, organizationId: props.defaultValues.id})
console.log(organization)
console.log(values);
const organization = await updateOrganizationAction({ data: values, organizationId: props.defaultValues.id });
console.log(organization);
if (organization.data.success) {
toast.success(organization.data.actionSuccess.message);
router.push(`/dashboard/${organization.data.value.slug}/settings`);
router.refresh()
router.refresh();
} else {
toast.success(organization.data.actionError.message);
}
}
})
},
});
return (
<Card>
<CardHeader>
</CardHeader>
<CardHeader></CardHeader>
<CardContent>
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Organization 1" {...field} />
<Input placeholder="Organization 1" {...field} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -113,29 +88,30 @@ export const OrganizationForm = (props: organizationFormProps) => {
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
placeholder="project-1" {...field}
placeholder="project-1"
{...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
field.onChange(value)
}}/>
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
field.onChange(value);
}}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="users"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Databases</FormLabel>
<FormControl>
<MultiSelect
options={formatUsersList(props.users)}
onValueChange={field.onChange}
@@ -147,16 +123,13 @@ export const OrganizationForm = (props: organizationFormProps) => {
/>
</FormControl>
<FormDescription>Select users you want to add to this organization</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)
}
)}
/>
<Button>
{isCreate ? `Create Organization` : `Update Organization`}
</Button>
<Button>{isCreate ? `Create Organization` : `Update Organization`}</Button>
</Form>
</CardContent>
</Card>
)
}
);
};
@@ -1,27 +1,27 @@
"use client"
"use client";
import {useMutation} from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from "@/components/ui/dialog";
import {Button} from "@/components/ui/button";
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
import {createOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
import {useRouter} from "next/navigation";
import {useState} from "react";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { OrganizationSchema } from "@/components/wrappers/dashboard/organization/organization.schema";
import { createOrganizationAction } from "@/components/wrappers/dashboard/organization/organization.action";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { authClient } from "@/lib/auth/auth-client";
export type createOrganizationModalProps = {
children: any
}
children: any;
};
export function CreateOrganizationModal(props: createOrganizationModalProps) {
const [open, setOpen] = useState(false);
const {children} = props;
const { children } = props;
const router = useRouter()
const router = useRouter();
const form = useZodForm({
schema: OrganizationSchema,
@@ -29,46 +29,49 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
const mutation = useMutation({
mutationFn: async (values: OrganizationSchema) => {
console.log(values)
const result = await createOrganizationAction(values)
setOpen(false);
router.refresh()
}
})
console.log(values);
const result = await createOrganizationAction(values);
if (result && result.data) {
if (result.data.success && result.data.value) {
setOpen(false);
await authClient.organization.setActive({ organizationSlug: result.data.value.slug });
router.replace(`/dashboard/${result.data.value.slug}/home`);
}
}
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-[425px] w-full">
<DialogHeader>
<DialogTitle>Create a new organization</DialogTitle>
</DialogHeader>
<div className="sm:max-w-[375px] w-full">
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
console.log(values)
await mutation.mutateAsync(values);
}}
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
console.log(values);
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -76,18 +79,19 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input {...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
field.onChange(value)
}}
<Input
{...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
field.onChange(value);
}}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -98,10 +102,7 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
</DialogFooter>
</Form>
</div>
</DialogContent>
</Dialog>
)
}
);
}
@@ -1,76 +1,36 @@
"use client"
"use client";
import {useEffect, useState} from "react";
import { ComboBox } from "@/components/wrappers/common/combobox";
import { useSidebar } from "@/components/ui/sidebar";
import { useRouter } from "next/navigation";
import { authClient } from "@/lib/auth/auth-client";
import {ComboBox} from "@/components/wrappers/common/combobox";
import {Organization} from "@prisma/client";
import {
getCurrentOrganizationSlug,
setCurrentOrganizationSlug
} from "@/features/dashboard/organization-cookie";
import {useSidebar} from "@/components/ui/sidebar";
import {useRouter} from "next/navigation";
export function OrganizationCombobox() {
const router = useRouter();
export type organizationComboBoxProps = {
organizations: Organization[]
defaultOrganization: Organization
}
const { data: organizations } = authClient.useListOrganizations();
const { data: activeOrganization } = authClient.useActiveOrganization();
if (!organizations) return null;
export function OrganizationCombobox(props: organizationComboBoxProps) {
const router = useRouter()
// const {organizationId, moveToAnotherOrganization} = useStore((state) => state);
console.log("organizations", organizations);
console.log("activeOrganization", activeOrganization);
const [organizationSlug, setOrganizationSlug] = useState<string>()
const {organizations, defaultOrganization} = props
useEffect(() => {
getCurrentOrganizationSlug().then(slug => {
if (slug == "") {
setOrganizationSlug(defaultOrganization.id)
setCurrentOrganizationSlug(defaultOrganization.slug)
} else {
setOrganizationSlug(slug)
const organization = organizations.find(organization => organization.slug === slug)
if (!organization) {
setOrganizationSlug(defaultOrganization.id)
setCurrentOrganizationSlug(defaultOrganization.slug)
}
}
})
}, [organizationSlug])
const values = organizations.map(organization => {
return ({
const values = organizations.map((organization) => {
return {
value: organization.slug,
label: organization.name,
})
})
};
});
const onValueChange = (slug: string) => {
if (organizationSlug !== slug) {
setOrganizationSlug(slug)
setCurrentOrganizationSlug(slug)
router.replace("/dashboard")
}
}
const {state, isMobile} = useSidebar();
const onValueChange = async (slug: string) => {
await authClient.organization.setActive({
organizationSlug: slug,
});
router.replace(`/dashboard/${slug}/home`);
router.refresh();
};
const { state } = useSidebar();
return (
<>
{state === 'expanded' && (
<ComboBox
sideBar
values={values}
defaultValue={organizationSlug}
onValueChange={onValueChange}/>
)}
</>
)
}
return <>{state === "expanded" && <ComboBox sideBar values={values} defaultValue={activeOrganization?.slug} onValueChange={onValueChange} />}</>;
}
@@ -1,218 +1,160 @@
"use server"
"use server";
import {ActionError, userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
import {ServerActionResult} from "@/types/action-type";
import {Organization} from "@prisma/client";
import {z} from "zod";
import {v4 as uuidv4} from "uuid";
import {getDb} from "@/db";
import {
OrganizationFormSchema
} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
import { userAction } from "@/safe-actions";
import { OrganizationSchema } from "@/components/wrappers/dashboard/organization/organization.schema";
import { ServerActionResult } from "@/types/action-type";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { OrganizationFormSchema } from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import { db } from "@/db";
import { Organization, organization as drizzleOrganization, organizationMember as drizzleOrganizationMember } from "@/db/schema";
import { eq } from "drizzle-orm";
import { checkSlugOrganization, createOrganization } from "@/lib/auth/auth";
const verifySlugUniqueness = async (slug: string) => {
const slugExists = await prisma.organization.count({
where: {
slug: slug
},
})
if (slugExists) {
throw new ActionError("Slug already exists.");
}
}
export const createOrganizationAction = userAction
.schema(OrganizationSchema)
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
try {
await verifySlugUniqueness(parsedInput.slug)
const organization = await prisma.organization.create({
data: {
...parsedInput
}
})
await prisma.userOrganization.create({
data: {
userId: ctx.user.id,
organizationId: organization.id,
role: "admin"
},
});
return {
success: true,
value: organization,
actionSuccess: {
message: "Organization has been successfully created.",
messageParams: {organizationId: organization.id},
},
};
} catch (error) {
console.error("Error creating organization:", error);
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({ parsedInput }): Promise<ServerActionResult<Organization>> => {
try {
if (!checkSlugOrganization(parsedInput.slug)) {
return {
success: false,
actionError: {
message: "Failed to create organization.",
message: "Slug is already taken",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: {message: "Error creating the organization"},
messageParams: { message: "Error creating the organization" },
},
};
}
});
const organization = await createOrganization(parsedInput.name, parsedInput.slug);
return {
success: true,
value: organization!,
actionSuccess: {
message: "Organization has been successfully created.",
messageParams: { organizationId: organization!.id },
},
};
} catch (error) {
console.error("Error creating organization:", error);
return {
success: false,
actionError: {
message: "Failed to create organization.",
status: 500,
messageParams: { message: "Error creating the organization" },
},
};
}
});
export const updateOrganizationAction = userAction
.schema(
z.object({
data: OrganizationFormSchema,
organizationId: z.string()
}))
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
organizationId: z.string(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<Organization>> => {
try {
const newUserList = parsedInput.data.users;
const newUserList = parsedInput.data.users
const organization = await db.select().from(organization).where(eq(organization.id, parsedInput.organizationId)).execute();
const organization = await prisma.organization.findFirst({
where:{
id: parsedInput.organizationId,
},
include: {
users : {}
}
})
const existingItemIds = organization.users.map((user) => user.userId);
const usersToAdd = newUserList.filter(
(id) => !existingItemIds.includes(id)
);
const usersToRemove = existingItemIds.filter(
(id) => !newUserList.includes(id)
);
console.log(usersToAdd);
console.log(usersToRemove);
if (organization.length === 0) {
throw new Error("Organization not found.");
}
const existingItemIds = organization[0].users.map((user) => user.userId);
const usersToAdd = newUserList.filter((id) => !existingItemIds.includes(id));
const usersToRemove = existingItemIds.filter((id) => !newUserList.includes(id));
if (usersToAdd.length > 0) {
await prisma.userOrganization.createMany({
data: usersToAdd.map((userId) => ({
userId: userId,
organizationId: organization.id,
role: "member"
})),
skipDuplicates: true, // Optional: to avoid duplicate insertion errors
});
await db
.insert(userOrganization)
.values(
usersToAdd.map((userId) => ({
userId,
organizationId: organization[0].id,
role: "member",
}))
)
.execute();
}
if (usersToRemove.length > 0) {
await prisma.userOrganization.deleteMany({
where: {
userId: { in: usersToRemove },
},
});
await db.delete().from(userOrganization).where(inArray(userOrganization.userId, usersToRemove)).execute();
}
const updatedOrganization = await prisma.organization.update({
where:{
id: organization.id
},
data:{
const updatedOrganization = await db
.update(organization)
.set({
name: parsedInput.data.name,
slug: parsedInput.data.slug,
}
})
})
.where(eq(organization.id, parsedInput.organizationId))
.returning()
.execute();
return {
success: true,
value: updatedOrganization,
value: updatedOrganization[0],
actionSuccess: {
message: "Organization has been successfully updated.",
messageParams: {organizationId: updatedOrganization.id},
messageParams: { organizationId: updatedOrganization[0].id },
},
};
} catch (error) {
console.error("Error updating organization:", error);
return {
success: false,
actionError: {
message: "Failed to update organization.",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: {message: "Error updating the organization"},
messageParams: { message: "Error updating the organization" },
},
};
}
});
export const deleteOrganizationAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }): Promise<ServerActionResult<Organization>> => {
try {
const uuid = uuidv4();
const organization = await db.select().from(organization).where(eq(organization.slug, parsedInput)).execute();
export const deleteOrganizationAction = userAction
.schema(z.string())
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
console.log(parsedInput);
try {
const db = await getDb()
const uuid = uuidv4()
const organization = await db.organization.findFirst({
where: {
slug: parsedInput,
}
})
console.log(organization);
const organizationUpdated = await db.organization.update({
where: {
slug: parsedInput,
},
data: {
name: `${organization.name}-${uuid}`,
slug: `${organization.slug}-${uuid}`,
deleted : true
}
})
return {
success: true,
value: organizationUpdated,
actionSuccess: {
message: "Organization has been successfully deleted.",
messageParams: {organizationId: organizationUpdated.id},
},
};
} catch (error) {
console.error("Error deleting organization:", error);
return {
success: false,
actionError: {
message: "Failed to delete organization.",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: {message: "Error deleting the organization"},
},
};
if (organization.length === 0) {
throw new Error("Organization not found.");
}
});
const updatedOrganization = await db
.update(organization)
.set({
name: `${organization[0].name}-${uuid}`,
slug: `${organization[0].slug}-${uuid}`,
deleted: true,
})
.where(eq(organization.id, organization[0].id))
.returning()
.execute();
return {
success: true,
value: updatedOrganization[0],
actionSuccess: {
message: "Organization has been successfully deleted.",
messageParams: { organizationId: updatedOrganization[0].id },
},
};
} catch (error) {
console.error("Error deleting organization:", error);
return {
success: false,
actionError: {
message: "Failed to delete organization.",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: { message: "Error deleting the organization" },
},
};
}
});
@@ -1,12 +1,12 @@
import {z} from "zod";
import { z } from "zod";
export const OrganizationSchema = z.object({
name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'),
slug: z.string()
.regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens')
.min(5, 'Slug must be at least 5 characters long')
.max(20, 'Slug must be at most 20 characters long'),
name: z.string().min(5, "Name must be at least 5 characters long").max(40, "Name must be at most 40 characters long"),
slug: z
.string()
.regex(/^[a-zA-Z0-9_-]*$/, "Slug can only contain letters, numbers, underscores, and hyphens")
.min(5, "Slug must be at least 5 characters long")
.max(20, "Slug must be at most 20 characters long"),
});
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;
@@ -1,32 +1,27 @@
"use client"
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
import {User} from "@prisma/client";
import {UploadIcon} from "lucide-react";
import {toast} from "sonner";
import {uploadImageAction} from "@/features/upload/public/upload.action";
import {useMutation} from "@tanstack/react-query";
import {prisma} from "@/prisma";
import {updateImageUserAction} from "@/components/wrappers/dashboard/profile/Avatar/avatar.action";
import {useRouter} from "next/navigation";
import {useSession} from "next-auth/react";
"use client";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { UploadIcon } from "lucide-react";
import { toast } from "sonner";
import { uploadImageAction } from "@/features/upload/public/upload.action";
import { useMutation } from "@tanstack/react-query";
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/Avatar/avatar.action";
import { useRouter } from "next/navigation";
import { User } from "@/db/schema/01_user";
export type AvatarWithUploadProps = {
user: User
}
user: User;
};
export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
const user = props.user
const user = props.user;
const router = useRouter();
const { data: session, update } = useSession();
const submitImage = useMutation({
mutationFn: async (file: File) => {
const formData = new FormData();
formData.set("file", file);
const uploadImage = await uploadImageAction(formData)
const data = uploadImage?.data?.data
const uploadImage = await uploadImageAction(formData);
const data = uploadImage?.data?.data;
if (uploadImage?.serverError || !data) {
console.log(uploadImage?.serverError);
@@ -34,8 +29,8 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
return;
}
const updateUser = await updateImageUserAction(data.url)
const dataUser = updateUser?.data?.data
const updateUser = await updateImageUserAction(data.url);
const dataUser = updateUser?.data?.data;
if (updateUser?.serverError || !dataUser) {
console.log(updateUser?.serverError);
@@ -43,42 +38,27 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
return;
}
const newSession = {
...session,
user: {
...session?.user,
image: data.url
},
};
await update(newSession);
toast.success("Successfully uploaded user image!");
router.refresh()
}
})
router.refresh();
},
});
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.includes("image")) {
toast.error("File not an image")
toast.error("File not an image");
return;
}
submitImage.mutate(file)
submitImage.mutate(file);
};
return (
<div className="relative " >
<Avatar className="size-14 mr-3 ">
<AvatarFallback>{user.name?.[0]}</AvatarFallback>
{user.image ? (
<AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`}/>
) : null}
</Avatar>
<div className="relative ">
<Avatar className="size-14 mr-3 ">
<AvatarFallback>{user.name[0]}</AvatarFallback>
{user.image ? <AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`} /> : null}
</Avatar>
<div
onClick={() => {
const fileInput = document.createElement("input");
@@ -90,8 +70,8 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
}}
className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-100 hover:bg-gray-500 hover:bg-opacity-50 rounded-full size-14"
>
<UploadIcon className="w-8 h-8 text-primary"/>
<UploadIcon className="w-8 h-8 text-primary" />
</div>
</div>
)
}
);
};
@@ -1,24 +1,14 @@
"use server"
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {prisma} from "@/prisma";
"use server";
import { db } from "@/db";
import { user as drizzleUser } from "@/db/schema";
import { userAction } from "@/safe-actions";
import { eq } from "drizzle-orm";
import { z } from "zod";
export const updateImageUserAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }) => {
const [updatedUser] = await db.update(drizzleUser).set({ image: parsedInput }).where(eq(drizzleUser.id, ctx.user.id)).returning();
export const updateImageUserAction = userAction
.schema(z.string())
.action(async ({parsedInput, ctx}) => {
const user = await prisma.user.update({
where: {
id: ctx.user.id,
},
data: {
image: parsedInput,
}
})
return {
data: user,
}
});
return {
data: updatedUser,
};
});
@@ -1,34 +1,42 @@
"use client"
"use client";
import {useMutation} from "@tanstack/react-query";
import {signOutAction} from "@/features/auth/auth.action";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
import {Trash2} from "lucide-react";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import { useMutation } from "@tanstack/react-query";
import { Trash2 } from "lucide-react";
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
import { signOut } from "@/lib/auth/auth-client";
import { useRouter } from "next/navigation";
import { deleteUserAction } from "./delete-account.action";
export type ButtonDeleteAccountProps = {
text? : string
}
text?: string;
};
export const ButtonDeleteAccount = (props: ButtonDeleteAccountProps) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: () => deleteUserAction(""),
onSuccess: async () => {
await signOutAction();
await signOut({
fetchOptions: {
onSuccess: () => {
router.push("/login");
},
},
});
},
})
});
return (
<ButtonWithConfirm
text={props.text ? props.text : ""}
onClick={() => {
mutation.mutate()
mutation.mutate();
}}
variant={"destructive"}
isPending={mutation.isPending}
className="gap-2"
icon={<Trash2/>}
icon={<Trash2 />}
/>
)
}
);
};
@@ -1,44 +1,27 @@
"use server"
import {userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {z} from "zod";
import {v4 as uuidv4} from "uuid";
import {EmailFormSchema} from "@/components/wrappers/dashboard/admin/AdminEmailTab/EmailForm/email-form.schema";
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { db } from "@/db";
import { user as drizzleUser } from "@/db/schema";
import { eq } from "drizzle-orm";
export const deleteUserAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }) => {
const userId = parsedInput.length > 0 ? parsedInput : ctx.user.id;
const uuid = uuidv4();
export const deleteUserAction = userAction
.schema(z.string())
.action(async ({parsedInput, ctx}) => {
const userId = parsedInput.length > 0 ? parsedInput : ctx.user.id;
const uuid = uuidv4()
const user = await prisma.user.update({
where: {
id: userId,
},
data: {
email: `${uuid}@portabase.com`,
name: `${uuid}`,
deleted: true
}
const [updatedUser] = await db
.update(drizzleUser)
.set({
email: `${uuid}@portabase.com`,
name: `${uuid}`,
//deleted: true,
//todo: add deleted
})
.where(eq(drizzleUser.id, userId))
.returning();
const account = await prisma.account.findFirst({
where: {
userId: userId,
}
})
if (account) {
await prisma.account.delete({
where: {
id: account.id
}
})
}
return {
data: user,
}
});
return {
data: updatedUser,
};
});
@@ -1,21 +1,22 @@
import {currentUser, requiredCurrentUser} from "@/auth/current-user";
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { currentUser } from "@/lib/auth/current-user";
import { notFound } from "next/navigation";
export type UserAvatarProps = {}
export type UserAvatarProps = {};
export const UserAvatar = async () => {
const user = await currentUser();
const user = await currentUser()
if (!user) {
return notFound();
}
return (
<div>
<Avatar className="size-6">
<AvatarFallback>{user.name?.[0]}</AvatarFallback>
{user.image ? (
<AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`}/>
) : null}
<AvatarFallback>{user.name[0]}</AvatarFallback>
{user.image ? <AvatarImage src={user.image} alt={`${user.name ?? "-"}'s profile picture`} /> : null}
</Avatar>
</div>
)
}
);
};
@@ -1,28 +1,24 @@
"use client";
import {Card, CardContent, CardDescription, CardHeader, CardTitle} 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 {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import {UserSchema, UserType} from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
import {toast} from "sonner";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
import { useSession } from "next-auth/react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { FormControl, 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 { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip";
import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
import { toast } from "sonner";
import { updateUserAction } from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
export type userFormProps = {
defaultValues?: UserType;
userId?: string;
}
};
export const UserForm = (props: userFormProps) => {
const isCreate = !Boolean(props.defaultValues)
const isCreate = !Boolean(props.defaultValues);
const form = useZodForm({
schema: UserSchema,
@@ -30,96 +26,73 @@ export const UserForm = (props: userFormProps) => {
});
const router = useRouter();
const { data: session, update } = useSession();
const mutation = useMutation({
mutationFn: async (values: UserType) => {
console.log("values", values)
console.log(props.userId)
console.log("values", values);
console.log(props.userId);
const updateUser = await updateUserAction({
id: props.userId ?? "-",
data: values
})
data: values,
});
const data = updateUser?.data?.data
const data = updateUser?.data?.data;
if (updateUser?.serverError || !data) {
console.log(updateUser?.serverError);
toast.error(updateUser?.serverError);
return;
}
console.log("email:", values.email)
const newSession = {
...session,
user: {
...session?.user,
name: values.name,
email: values.email
},
};
const updateSession = await update(newSession);
console.log(updateSession);
toast.success(`Success updating user informations`);
toast.success(`Profile updated successfully.`);
router.push(`/dashboard/profile`);
router.refresh()
}
})
router.refresh();
},
});
return (
<TooltipProvider>
<Card>
<CardHeader>
<CardTitle>
Account
</CardTitle>
<CardDescription>
Your informations
</CardDescription>
<CardTitle>Account</CardTitle>
<CardDescription>Your informations</CardDescription>
</CardHeader>
<CardContent>
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder={"Your Name"} {...field} />
<Input placeholder={"Your Name"} {...field} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder={'exemple@portabase.com'} disabled {...field}
value={field.value ?? ""}/>
<Input placeholder={"exemple@portabase.com"} disabled {...field} value={field.value ?? ""} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<Button>
{isCreate ? `` : `Save`}
</Button>
<Button>{isCreate ? `` : `Save`}</Button>
</Form>
</CardContent>
</Card>
</TooltipProvider>
)
}
);
};
@@ -1,26 +1,22 @@
"use server"
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {prisma} from "@/prisma";
import {UserSchema} from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { UserSchema } from "@/components/wrappers/dashboard/profile/UserForm/user-form.schema";
import { db } from "@/db";
import { user as drizzleUser } from "@/db/schema";
import { eq } from "drizzle-orm";
export const updateUserAction = userAction
.schema(
z.object({
id: z.string(),
data: UserSchema,
}
)
)
.action(async ({parsedInput, ctx}) => {
const updatedUser = await prisma.user.update({
where: {
id: parsedInput.id,
},
data: parsedInput.data,
id: z.string(),
data: UserSchema,
})
)
.action(async ({ parsedInput }) => {
const [updatedUser] = await db.update(drizzleUser).set(parsedInput.data).where(eq(drizzleUser.id, parsedInput.id)).returning();
return {
data: updatedUser,
}
})
};
});
@@ -1,43 +1,41 @@
"use client"
"use client";
import {Trash2} from "lucide-react";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {useMutation} from "@tanstack/react-query";
import {deleteProjectAction} from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/delete-project.action";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
import { Trash2 } from "lucide-react";
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
import { useMutation } from "@tanstack/react-query";
import { deleteProjectAction } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/delete-project.action";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
export type ButtonDeleteProjectProps = {
text? : string
projectId: string
}
text?: string;
projectId: string;
};
export const ButtonDeleteProject = (props: ButtonDeleteProjectProps) => {
const router = useRouter()
const router = useRouter();
const mutation = useMutation({
mutationFn: () => deleteProjectAction(props.projectId),
onSuccess: async (result: any) => {
console.log(result)
if(result.data?.success) {
if (result.data?.success) {
toast.success(result.data.actionSuccess.message);
router.push("/")
}else{
router.push("/");
} else {
toast.error(result.data.actionError.message || "Unknown error occurred.");
}
},
})
});
return (
<ButtonWithConfirm
text={props.text ? props.text : ""}
onClick={() => {
mutation.mutate()
mutation.mutate();
}}
variant={"destructive"}
isPending={mutation.isPending}
className="gap-2"
icon={<Trash2/>}
icon={<Trash2 />}
/>
)
}
);
};
@@ -1,47 +1,49 @@
"use server"
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {v4 as uuidv4} from "uuid";
import {prisma} from "@/prisma";
import {ServerActionResult} from "@/types/action-type";
import {Projects} from "@prisma/client";
"use server";
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { ServerActionResult } from "@/types/action-type";
import { eq } from "drizzle-orm";
import { db } from "@/db";
import { project } from "@/db/schema";
export const deleteProjectAction = userAction
.schema(z.string())
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Projects>> => {
export const deleteProjectAction = userAction.schema(z.string()).action(async ({ parsedInput }): Promise<ServerActionResult<typeof project.$inferSelect>> => {
try {
const uuid = uuidv4();
try {
const uuid = uuidv4()
const project = await prisma.project.update({
where: {
id: parsedInput
},
data:{
isArchived: true,
slug: uuid
}
const updatedProjects = await db
.update(project)
.set({
isArchived: true,
slug: uuid,
})
.where(eq(project.id, parsedInput))
.returning();
return {
success: true,
value: project,
actionSuccess: {
message: "Projects has been successfully archived.",
messageParams: {projectId: parsedInput},
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to archived Projects.",
status: 500, // Optional: Use a meaningful status code
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {projectId: parsedInput},
},
};
const updatedProject = updatedProjects[0];
if (!updatedProject) {
throw new Error("Project not found or update failed");
}
});
return {
success: true,
value: updatedProject,
actionSuccess: {
message: "Projects has been successfully archived.",
messageParams: { projectId: parsedInput },
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to archive Projects.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: { projectId: parsedInput },
},
};
}
});
@@ -1,41 +1,30 @@
"use client"
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {formatDateLastContact} from "@/utils/date-formatting";
import {Database} from "@prisma/client";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Database } from "@/db/schema";
import { formatDateLastContact } from "@/utils/date-formatting";
export type DatabaseKpiPro = {
successRate: any,
database: Database,
totalBackups: number
}
successRate: any;
database: Database;
totalBackups: number;
};
export const DatabaseKpi = (props: DatabaseKpiPro) => {
return (
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl">
Backups
</CardHeader>
<CardHeader className="font-bold text-xl">Backups</CardHeader>
<CardContent>{props.totalBackups}</CardContent>
</Card>
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl">
Success rate
</CardHeader>
<CardContent>
{props.successRate ? `${props.successRate} %` : "Unavailable for now."}
</CardContent>
<CardHeader className="font-bold text-xl">Success rate</CardHeader>
<CardContent>{props.successRate ? `${props.successRate} %` : "Unavailable for now."}</CardContent>
</Card>
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl">
Last contact
</CardHeader>
<CardContent>
{formatDateLastContact(props.database.lastContact)}
</CardContent>
<CardHeader className="font-bold text-xl">Last contact</CardHeader>
<CardContent>{formatDateLastContact(props.database.lastContact)}</CardContent>
</Card>
</div>
)
}
);
};
@@ -1,32 +1,30 @@
"use client"
"use client";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
import {Backup, Database, Restoration} from "@prisma/client";
import {useEffect} from "react";
import {useRouter} from "next/navigation";
import {eventUpdate} from "@/types/events";
import {backupColumns} from "@/features/dashboard/backup/columns";
import {restoreColumns} from "@/features/dashboard/restore/columns";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { eventUpdate } from "@/types/events";
import { backupColumns } from "@/features/dashboard/backup/columns";
import { restoreColumns } from "@/features/dashboard/restore/columns";
import { DataTable } from "@/components/wrappers/common/table/data-table";
import { Backup, Database, Restoration } from "@/db/schema";
export type DatabaseTabsProps = {
backups: Backup[]
restorations: Restoration[]
isAlreadyRestore: boolean
database: Database
}
backups: Backup[];
restorations: Restoration[];
isAlreadyRestore: boolean;
database: Database;
};
export const DatabaseTabs = (props: DatabaseTabsProps) => {
const router = useRouter();
useEffect(() => {
const eventSource = new EventSource('/api/events');
const eventSource = new EventSource("/api/events");
eventSource.addEventListener('modification', (event) => {
const data: eventUpdate = JSON.parse(event.data)
eventSource.addEventListener("modification", (event) => {
const data: eventUpdate = JSON.parse(event.data);
if (data.update) {
console.log("update", data.update)
router.refresh()
router.refresh();
}
});
@@ -37,20 +35,21 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
return (
<Tabs className="flex flex-col flex-1" defaultValue="backup">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="backup">Backup</TabsTrigger>
<TabsTrigger value="restore">Restoration</TabsTrigger>
</TabsList>
<TabsContent className="h-full justify-between" value="backup">
<DataTableWithPagination columns={backupColumns} data={props.backups}
extendedProps={props.isAlreadyRestore}/>
{/*
<DataTable columns={backupColumns} data={props.backups} extendedProps={props.isAlreadyRestore} />
*/}
<DataTable columns={backupColumns} data={props.backups} enablePagination />
</TabsContent>
<TabsContent className="h-full justify-between" value="restore">
<DataTableWithPagination columns={restoreColumns} data={props.restorations}/>
<DataTable columns={restoreColumns} data={props.restorations} enablePagination />
</TabsContent>
</Tabs>
)
}
);
};
@@ -1,29 +1,25 @@
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { ProjectWith } from "@/db/schema";
import Link from "next/link";
export type projectCardProps = {
data: any,
organizationSlug?: string
}
data: ProjectWith;
organizationSlug?: string;
};
export const ProjectCard = (props: projectCardProps) => {
const {data: project, organizationSlug} = props;
const { data: project, organizationSlug } = props;
return (
<Link href={`/dashboard/${organizationSlug}/projects/${project.id}`}>
<Card className="flex flex-row justify-between">
<div className="">
<CardHeader>{project.name}</CardHeader>
<CardContent>
{project.databases.length} databases
</CardContent>
<CardContent>{project.databases.length} databases</CardContent>
</div>
</Card>
</Link>
)
}
);
};
@@ -1,57 +1,47 @@
"use client";
import Link from "next/link";
import Image from "next/image"
import {Database} from "@prisma/client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {ConnectionCircle} from "@/components/wrappers/common/connection-circle";
import {formatDateLastContact} from "@/utils/date-formatting";
import Image from "next/image";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
import { formatDateLastContact } from "@/utils/date-formatting";
import { Database } from "@/db/schema";
export type projectDatabaseCardProps = {
data: Database,
extendedProps: any
organizationSlug: string
}
data: Database;
extendedProps: any;
organizationSlug: string;
};
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
const {organizationSlug,data: database, extendedProps: extendedProps} = props;
const { organizationSlug, data: database, extendedProps: extendedProps } = props;
return (
<Link href={`/dashboard/${organizationSlug}/projects/${extendedProps.id}/database/${database.id}`}>
<DatabaseCard data={database}/>
<DatabaseCard data={database} />
</Link>
)
}
);
};
export type databaseCardProps = {
data: Database,
}
data: Database;
};
export const DatabaseCard = (props: databaseCardProps) => {
const {data: database} = props;
const { data: database } = props;
return (
<Card className="flex flex-row justify-between">
<div className="flex flex-row items-center gap-2">
<Image
src="/PostgreSQL.png"
alt="Database type Icon"
width={60}
height={60}
className="object-cover ml-4"
/>
<Image src="/PostgreSQL.png" alt="Database type Icon" width={60} height={60} className="object-cover ml-4" />
<div>
<CardHeader>Name : {database.name}</CardHeader>
<CardContent>
Last contact: {formatDateLastContact(database.lastContact)}
</CardContent>
<CardContent>Last contact: {formatDateLastContact(database.lastContact)}</CardContent>
</div>
</div>
<div className="mt-3 mr-3">
<ConnectionCircle date={database.lastContact}/>
<ConnectionCircle date={database.lastContact} />
</div>
</Card>
)
}
);
};
@@ -1,5 +1,4 @@
import {z} from "zod";
import Database from "@prisma/client"
import { z } from "zod";
export const ProjectSchema = z.object({
name: z.string(),
@@ -1,115 +1,105 @@
"use client";
import {Card, CardContent, CardHeader} 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 {ProjectSchema, ProjectType} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
import {
createProjectAction,
updateProjectAction
} from "@/components/wrappers/dashboard/projects/ProjectsForm/project-form.action";
import {useRouter} from "next/navigation";
import {Database, Organization, Projects} from "@prisma/client"
import {MultiSelect} from "@/components/wrappers/common/multiSelect/MultiSelect";
import {toast} from "sonner";
import { Card, CardContent, CardHeader } 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 { ProjectSchema, ProjectType } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
import { createProjectAction, updateProjectAction } from "@/components/wrappers/dashboard/projects/ProjectsForm/project-form.action";
import { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
import { toast } from "sonner";
import { DatabaseWith, Organization } from "@/db/schema";
export type projectFormProps = {
defaultValues?: ProjectType;
databases: Database[],
organization: Organization,
databases: DatabaseWith[];
organization: Organization;
projectId?: string;
}
};
export const ProjectForm = (props: projectFormProps) => {
const router = useRouter();
const isCreate = !Boolean(props.defaultValues)
const isCreate = !Boolean(props.defaultValues);
const formatDatabasesList = (databases: Database[]) => {
return databases.map(database => ({
const formatDatabasesList = (databases: DatabaseWith[]) => {
return databases.map((database) => ({
value: database.id,
label: `${database.name} (${database.generatedId}) | ${database.agent.name}`,
label: `${database.name} (${database.id}) | ${database.agent.name}`,
}));
};
const formatDefaultDatabases = (databases: ProjectType['databases']): string[] => {
return databases.map(database => database.id);
const formatDefaultDatabases = (databases: string[]): string[] => {
return databases;
};
const formattedDefaultValues = {
...props.defaultValues,
databases: !isCreate ? formatDefaultDatabases(props.defaultValues?.databases) : []
}
databases: !isCreate ? formatDefaultDatabases(props.defaultValues?.databases ?? []) : [],
};
const form = useZodForm({
schema: ProjectSchema,
defaultValues: formattedDefaultValues,
});
const mutation = useMutation({
mutationFn: async (values: ProjectType) => {
console.log(values)
const project: Projects = isCreate ? await createProjectAction({
data: values,
organizationId: props.organization.id
}) : await updateProjectAction({
data: values,
organizationId: props.organization.id,
projectId: props.projectId
});
if (project.data?.success) {
toast.success(project.data.actionSuccess.message);
router.push(`/dashboard/${props.organization.slug}/projects/${project.data.value.id}`);
router.refresh();
} else {
toast.error(project.data.actionError.message || "Unknown error occurred.");
console.log(values);
if (!isCreate && !props.projectId) {
throw new Error("Project ID is required for updates");
}
const project = isCreate
? await createProjectAction({
data: values,
organizationId: props.organization.id,
})
: await updateProjectAction({
data: values,
organizationId: props.organization.id,
projectId: props.projectId!,
});
}
})
if (project && project.data) {
if (project.data.success) {
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
router.push(`/dashboard/${props.organization.slug}/projects/${project.data.value!.id}`);
router.refresh();
} else {
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
router.refresh();
}
} else {
toast.error("Failed to process request. No response received.");
router.refresh();
}
},
});
return (
<Card>
<CardHeader>
</CardHeader>
<CardHeader></CardHeader>
<CardContent>
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Project 1" {...field} />
<Input placeholder="Project 1" {...field} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -117,29 +107,30 @@ export const ProjectForm = (props: projectFormProps) => {
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
placeholder="project-1" {...field}
placeholder="project-1"
{...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
field.onChange(value)
}}/>
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
field.onChange(value);
}}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="databases"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Databases</FormLabel>
<FormControl>
<MultiSelect
options={formatDatabasesList(props.databases)}
onValueChange={field.onChange}
@@ -151,16 +142,13 @@ export const ProjectForm = (props: projectFormProps) => {
/>
</FormControl>
<FormDescription>Select databases you want to add to this project</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)
}
)}
/>
<Button>
{isCreate ? `Create Project` : `Update Project`}
</Button>
<Button>{isCreate ? `Create Project` : `Update Project`}</Button>
</Form>
</CardContent>
</Card>
)
}
);
};
@@ -1,12 +1,12 @@
"use server"
import {userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
import {z} from "zod";
import {ServerActionResult} from "@/types/action-type";
import {Projects} from "@prisma/client";
"use server";
import { userAction } from "@/safe-actions";
import { ProjectSchema } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
import { z } from "zod";
import { ServerActionResult } from "@/types/action-type";
import { Database, database as drizzleDatabase, project as drizzleProject, Project } from "@/db/schema";
import { db } from "@/db";
import { eq, inArray } from "drizzle-orm";
export const createProjectAction = userAction
.schema(
@@ -15,53 +15,42 @@ export const createProjectAction = userAction
organizationId: z.string(),
})
)
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Projects>> => {
.action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => {
try {
const project = await prisma.project.create({
data: {
const [createdProject] = await db
.insert(drizzleProject)
.values({
name: parsedInput.data.name,
slug: parsedInput.data.slug,
organizationId: parsedInput.organizationId,
}
})
for (const db of parsedInput.data.databases) {
await prisma.database.update({
where: {
id: db,
},
data:{
projectId: project.id,
}
})
.returning();
if (parsedInput.data.databases.length > 0) {
await db.update(drizzleDatabase).set({ projectId: createdProject.id }).where(inArray(drizzleDatabase.id, parsedInput.data.databases));
}
return {
success: true,
value: project,
value: createdProject,
actionSuccess: {
message: "Projects has been successfully created.",
messageParams: {projectName: parsedInput.data.name},
message: "Project has been successfully created.",
messageParams: { projectName: parsedInput.data.name },
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to create Projects.",
status: 500, // Optional: Use a meaningful status code
message: "Failed to create project.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {projectName: parsedInput.data.name},
messageParams: { projectName: parsedInput.data.name },
},
};
}
});
export const updateProjectAction = userAction
.schema(
z.object({
@@ -70,68 +59,48 @@ export const updateProjectAction = userAction
projectId: z.string(),
})
)
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Projects>> => {
.action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => {
try {
const newDatabaseList = parsedInput.data.databases
const project = await prisma.project.findFirst({
where:{
id: parsedInput.projectId,
const existing = await db.query.project.findFirst({
where: eq(drizzleProject.id, parsedInput.projectId),
with: {
databases: true,
},
include: {
databases : {}
}
})
const existingItemIds = project.databases.map((db) => db.id);
});
const databasesToAdd = newDatabaseList.filter(
(id) => !existingItemIds.includes(id)
);
const databasesToRemove = existingItemIds.filter(
(id) => !newDatabaseList.includes(id)
);
if (!existing) {
throw new Error("Project not found.");
}
console.log(databasesToAdd);
console.log(databasesToRemove);
const existingDbIds = existing.databases.map((db: Database) => db.id);
const newDbIds = parsedInput.data.databases;
const databasesToAdd = newDbIds.filter((id) => !existingDbIds.includes(id));
const databasesToRemove = existingDbIds.filter((id: string) => !newDbIds.includes(id));
if (databasesToAdd.length > 0) {
await prisma.database.updateMany({
where: {
id: { in: databasesToAdd },
},
data: {
projectId: parsedInput.projectId,
},
});
await db.update(drizzleDatabase).set({ projectId: parsedInput.projectId }).where(inArray(drizzleDatabase.id, databasesToAdd));
}
if (databasesToRemove.length > 0) {
await prisma.database.updateMany({
where: {
id: { in: databasesToRemove },
},
data: {
projectId: null,
},
});
await db.update(drizzleDatabase).set({ projectId: null }).where(inArray(drizzleDatabase.id, databasesToRemove));
}
const updatedProject = await prisma.project.update({
where:{
id: parsedInput.projectId
},
data:{
const [updatedProject] = await db
.update(drizzleProject)
.set({
name: parsedInput.data.name,
slug: parsedInput.data.slug,
}
})
})
.where(eq(drizzleProject.id, parsedInput.projectId))
.returning();
return {
success: true,
value: updatedProject,
actionSuccess: {
message: "Project has been successfully updated.",
messageParams: {projectName: parsedInput.data.name},
messageParams: { projectName: parsedInput.data.name },
},
};
} catch (error) {
@@ -139,12 +108,10 @@ export const updateProjectAction = userAction
success: false,
actionError: {
message: "Failed to update project.",
status: 500, // Optional: Use a meaningful status code
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {projectName: parsedInput.data.name},
messageParams: { projectName: parsedInput.data.name },
},
};
}
});
});
@@ -1,34 +1,28 @@
"use client"
"use client";
import {ColumnDef} from "@tanstack/react-table"
import {Badge} from "@/components/ui/badge";
import {User, UserOrganization} from "@prisma/client";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {useState} from "react";
import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {
updateUserOrganizationAction
} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/settings-user-tab.action";
import { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { useState } from "react";
export const usersColumns: ColumnDef<UserOrganization>[] = [
import { updateUserOrganizationAction } from "@/components/wrappers/dashboard/settings/SettingsUsersTab/settings-user-tab.action";
import { OrganizationMember } from "@/db/schema/02_organization";
export const usersColumns: ColumnDef<OrganizationMember>[] = [
{
accessorKey: "role",
header: "Role",
cell: ({row}) => {
cell: ({ row }) => {
const router = useRouter();
const [role, setRole] = useState<string>(row.getValue("role"))
const [role, setRole] = useState<string>(row.getValue("role"));
const updateMutation = useMutation({
mutationFn: () => updateUserOrganizationAction({id: row.original.id, role: role}),
mutationFn: () => updateUserOrganizationAction({ id: row.original.id, role: role }),
onSuccess: () => {
toast.success(`User updated successfully.`);
// router.refresh()
},
onError: () => {
toast.error(`An error occurred while updating user information.`);
@@ -36,32 +30,31 @@ export const usersColumns: ColumnDef<UserOrganization>[] = [
});
const handleUpdateRole = async () => {
const nextRole = role === "admin" ? "member"
: role === "member" ? "admin"
: "admin";
const nextRole = role === "admin" ? "member" : role === "member" ? "admin" : "admin";
setRole(nextRole);
await updateMutation.mutateAsync()
await updateMutation.mutateAsync();
};
return <Badge
className="cursor-pointer"
onClick={() => handleUpdateRole()}
variant="outline">{role}</Badge>
return (
<Badge className="cursor-pointer" onClick={() => handleUpdateRole()} variant="outline">
{role}
</Badge>
);
},
},
{
accessorKey: "user.name",
header: "Name"
header: "Name",
},
{
accessorKey: "user.email",
header: "Email"
header: "Email",
},
{
accessorKey: "updatedAt",
header: "Updated At",
cell: ({row}) => {
cell: ({ row }) => {
return new Date(row.getValue("updatedAt")).toLocaleString("fr-FR");
},
}
]
},
];
@@ -1,18 +1,19 @@
"use client"
import {useSidebar} from "@/components/ui/sidebar";
"use client";
import { useSidebar } from "@/components/ui/sidebar";
import { env } from "@/env.mjs";
export type SideBarFooterCreditProps = {}
export type SideBarFooterCreditProps = {};
export const SideBarFooterCredit = (props: SideBarFooterCreditProps) => {
const {state, isMobile} = useSidebar();
const { state } = useSidebar();
return (
<>
{state === 'expanded' && (
{state === "expanded" && (
<div className="text-center">
<h1 className="text-[10px]">Portabase Community Edition</h1>
<h1 className="text-[10px]">Portabase Community Edition v{env.NEXT_PUBLIC_PROJECT_VERSION}</h1>
</div>
)}
</>
)
}
);
};
@@ -1,53 +1,73 @@
"use client"
"use client";
import {useEffect, useState} from "react";
import { JSX, useEffect, useState } from "react";
import Link from "next/link";
import {SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem} from "@/components/ui/sidebar";
import {cn} from "@/lib/utils";
import {buttonVariants} from "@/components/ui/button";
import {ChartArea, Layers, Settings, ShieldHalf} from "lucide-react";
import {usePathname} from "next/navigation";
import {UserOrganization} from "@prisma/client";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu as SM,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { ChevronDown, MoreHorizontal } from "lucide-react";
import { usePathname } from "next/navigation";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
export type SidebarMenuCustomProps = {
currentOrganizationSlug: string,
currentOrganizationUser: UserOrganization,
}
export type SidebarMenuProps = {
baseUrl: string;
items: SidebarItem[];
};
export const SidebarMenuCustom = (props: SidebarMenuCustomProps) => {
type SidebarItemContent = {
title: string;
url: string;
icon: JSX.Element;
dropdown?: {
title: string;
url: string;
}[];
};
const BASE_URL = `/dashboard/${props.currentOrganizationSlug}`;
type SidebarListItem = {
type: "list";
content: SidebarItemContent;
};
type SidebarCollapseItem = {
type: "collapse";
title: string;
content: SidebarItemContent[];
};
export type SidebarItem = SidebarListItem | SidebarCollapseItem;
export const SidebarMenu = (props: SidebarMenuProps) => {
const pathname = usePathname();
// Menu items.
const items = [
{
title: "Projects",
url: "projects",
icon: Layers,
},
{
title: "Statistics",
url: "statistics",
icon: ChartArea,
}
]
if (props.currentOrganizationUser.role === "admin") {
items.push({
title: "Settings",
url: "settings",
icon: Settings,
},)
}
useEffect(() => {
const currentUrl = pathname;
const currentItem = items.find((item) => `${BASE_URL}/${item.url}` === currentUrl);
const currentItem = props.items.find((item) => {
if (item.type === "list") {
return `${props.baseUrl}/${item.content.url}` === currentUrl;
} else if (item.type === "collapse") {
return item.content.some((content) => `${props.baseUrl}/${content.url}` === currentUrl);
}
return false;
});
if (currentItem) {
setActiveItem(currentItem.title);
}
else{
if (currentItem.type === "list") {
setActiveItem(currentItem.content.title);
} else if (currentItem.type === "collapse") {
setActiveItem(currentItem.title!);
}
} else {
setActiveItem("");
}
}, [pathname]);
@@ -55,66 +75,88 @@ export const SidebarMenuCustom = (props: SidebarMenuCustomProps) => {
const [activeItem, setActiveItem] = useState("");
const handleItemClick = (title: string) => {
setActiveItem(title);
console.log(title)
}
return (
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link
className={cn(buttonVariants({
size: "lg",
variant: activeItem === item.title ? "secondary" : 'ghost'
}), "justify-start p-0")}
href={`${BASE_URL}/${item.url}`}
onClick={() => handleItemClick(item.title)}
>
<item.icon/>
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
<SidebarMenuAction
className={`peer-data-[active=true]/menu-button:opacity-100 ${activeItem === item.title ? 'active' : ''}`}/>
</SidebarMenuItem>
))}
</SidebarMenu>
)
}
};
// export type SidebarMenuItemCustomProps = {
// title: string
// url: string
// icon: Element
// }
//
//
// export const SidebarMenuItemCustom = (props: SidebarMenuItemCustomProps) => {
//
// const {title, url, icon} = props;
//
// const BASE_URL = "/dashboard";
// const pathname = usePathname();
//
// const [activeItem, setActiveItem] = useState(title);
//
// return (
// <SidebarMenuItem key={title}>
// <SidebarMenuButton asChild>
// <Link
// className={cn(buttonVariants({
// size: "lg",
// variant: activeItem === title ? "secondary" : 'ghost'
// }), "justify-start p-0")}
// href={`${BASE_URL}/${url}`}
// onClick={() => handleItemClick(item.title)}
// >
// <icon/>
// <span>{title}</span>
// </Link>
// </SidebarMenuButton>
// <SidebarMenuAction
// className={`peer-data-[active=true]/menu-button:opacity-100 ${activeItem === title ? 'active' : ''}`}/>
// </SidebarMenuItem>
// )
// }
return (
<SM>
{props.items.map((item, index) =>
item.type === "collapse" ? (
<Collapsible defaultOpen key={index} className={`group/collapsible`}>
<SidebarGroup>
<SidebarGroupLabel asChild>
<CollapsibleTrigger>
{item.title ?? "Please define a title"}
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
{item.content.map((content) => (
<SidebarMenuItem key={content.title}>
<SidebarMenuButton asChild>
<Link
className={cn(
buttonVariants({
size: "lg",
variant: activeItem === content.title ? "secondary" : "ghost",
}),
"justify-start p-0"
)}
href={`${props.baseUrl}/${content.url}`}
onClick={() => handleItemClick(content.title)}
>
{content.icon}
<span>{content.title}</span>
</Link>
</SidebarMenuButton>
{content.dropdown && (
<SidebarMenuAction>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction>
<MoreHorizontal />
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start">
{content.dropdown.map((dropdown) => (
<Link href={`${props.baseUrl}/${dropdown.url}`} className="justify-start p-0">
<DropdownMenuItem>
<span>{dropdown.title ?? "Please define a title"}</span>
</DropdownMenuItem>
</Link>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuAction>
)}
</SidebarMenuItem>
))}
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
) : (
<SidebarMenuItem key={index}>
<SidebarMenuButton asChild>
<Link
className={cn(
buttonVariants({
size: "lg",
variant: activeItem === item.content.title ? "secondary" : "ghost",
}),
"justify-start p-0"
)}
href={`${props.baseUrl}/${item.content.url}`}
onClick={() => handleItemClick(item.content.title)}
>
{item.content.icon}
<span>{item.content.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
)}
</SM>
);
};
@@ -1,69 +0,0 @@
"use client"
import {useEffect, useState} from "react";
import Link from "next/link";
import {SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem} from "@/components/ui/sidebar";
import {cn} from "@/lib/utils";
import {buttonVariants} from "@/components/ui/button";
import {ChartArea, Layers, Settings, ShieldHalf} from "lucide-react";
import {usePathname} from "next/navigation";
export type SidebarMenuAdminProps = {}
export const SidebarMenuAdmin = (props: SidebarMenuAdminProps) => {
const BASE_URL = "/dashboard";
const pathname = usePathname();
// Menu items.
const items = [
{
title: "Agents",
url: "agents",
icon: ShieldHalf,
},
{
title: "Administration panel",
url: "admin",
icon: Settings,
},
]
useEffect(() => {
const currentUrl = pathname;
const currentItem = items.find((item) => `${BASE_URL}/${item.url}` === currentUrl);
if (currentItem) {
setActiveItem(currentItem.title);
}else{
setActiveItem("");
}
}, [pathname]);
const [activeItem, setActiveItem] = useState("");
const handleItemClick = (title: string) => {
setActiveItem(title);
console.log(title)
}
return (
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link
className={cn(buttonVariants({
size: "lg",
variant: activeItem === item.title ? "secondary" : 'ghost'
}), "justify-start p-0")}
href={`${BASE_URL}/${item.url}`}
onClick={() => handleItemClick(item.title)}
>
<item.icon/>
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
<SidebarMenuAction
className={`peer-data-[active=true]/menu-button:opacity-100 ${activeItem === item.title ? 'active' : ''}`}/>
</SidebarMenuItem>
))}
</SidebarMenu>
)
}
@@ -1,91 +1,58 @@
import {
Sidebar,
SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenu as SM,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import {SidebarMenuCustom} from "@/components/wrappers/dashboard/sideBar/SideBarMenu/SideBarMenu";
import {OrganizationCombobox} from "@/components/wrappers/dashboard/organization/organization-combobox";
import {prisma} from "@/prisma";
import {currentUser, requiredCurrentUser} from "@/auth/current-user";
import {SideBarLogo} from "@/components/wrappers/dashboard/sideBar/SideBarLogo/SideBarLogo";
import {SideBarFooterCredit} from "@/components/wrappers/dashboard/sideBar/SideBarFooterCredit/SideBarFooterCredit";
import {LoggedInButton} from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
import {SidebarMenuAdmin} from "@/components/wrappers/dashboard/sideBar/SideBarMenu/SideBarMenuAdmin";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
} from "@/components/ui/sidebar";
import { SidebarItem, SidebarMenu } from "@/components/wrappers/dashboard/sideBar/SideBarMenu/SideBarMenu";
import { OrganizationCombobox } from "@/components/wrappers/dashboard/organization/organization-combobox";
import { SideBarLogo } from "@/components/wrappers/dashboard/sideBar/SideBarLogo/SideBarLogo";
import { SideBarFooterCredit } from "@/components/wrappers/dashboard/sideBar/SideBarFooterCredit/SideBarFooterCredit";
import { LoggedInButton } from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
import { Layers, ChartArea, Settings, ShieldHalf } from "lucide-react";
import { authClient } from "@/lib/auth/auth-client";
import { SidebarContentA } from "./sidebar-content";
export async function AppSidebar() {
/*const member = await getActiveMember();
const user = await requiredCurrentUser()
const currentOrganizationSlug = await getCurrentOrganizationSlug()
const organizations = await prisma.organization.findMany({
where: {
users: {
some: {
userId: user.id
},
},
deleted: {not: true},
},
})
const defaultOrganization = await prisma.organization.findUnique({
where: {
slug: "default"
}
})
console.log("member", member);
if (!member) {
return notFound();
}
const currentOrganizationUser = await prisma.userOrganization.findFirst({
where:{
userId: user.id,
organization:{
slug: currentOrganizationSlug != "" ? currentOrganizationSlug : "default",
}
}
})
const organization = await getOrganization(member.organizationId);
//todo: à revoir
console.log("memebrer", member);
console.log("aoaoaoaoaoa", organization);*/
return (
<Sidebar collapsible="icon">
<SidebarHeader>
<SideBarLogo/>
<SidebarMenu>
<SideBarLogo />
<SM>
<SidebarMenuItem>
<OrganizationCombobox
organizations={organizations}
defaultOrganization={defaultOrganization}
/>
<OrganizationCombobox />
</SidebarMenuItem>
</SidebarMenu>
</SM>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenuCustom currentOrganizationUser={currentOrganizationUser} currentOrganizationSlug={currentOrganizationSlug}/>
</SidebarGroupContent>
</SidebarGroup>
{user.role == "admin" ?
<SidebarGroup>
<SidebarGroupLabel>Administration</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenuAdmin/>
</SidebarGroupContent>
</SidebarGroup>
:null}
</SidebarContent>
<SidebarContentA />
<SidebarFooter>
<SidebarMenu>
<SM>
<SidebarMenuItem>
<LoggedInButton/>
<LoggedInButton />
</SidebarMenuItem>
</SidebarMenu>
<SideBarFooterCredit/>
</SM>
<SideBarFooterCredit />
</SidebarFooter>
</Sidebar>
)
}
);
}
@@ -0,0 +1,88 @@
"use client";
import { SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarContent } from "@/components/ui/sidebar";
import { authClient, useSession } from "@/lib/auth/auth-client";
import { Layers, ChartArea, Settings, ShieldHalf } from "lucide-react";
import { SidebarItem, SidebarMenu } from "./SideBarMenu/SideBarMenu";
export const SidebarContentA = () => {
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: organizations } = authClient.useListOrganizations();
const { data: session } = useSession();
console.log("sesssssion", session);
const appItems: SidebarItem[] = [
{
type: "list",
content: {
title: "Projects",
url: "projects",
icon: <Layers />,
},
},
{
type: "list",
content: {
title: "Statistics",
url: "statistics",
icon: <ChartArea />,
},
},
];
if (activeOrganization) {
const member = authClient.useActiveMember();
if (member && member.data && (member.data.role === "admin" || member.data.role === "owner")) {
appItems.push({
type: "list",
content: {
title: "Settings",
url: "settings",
icon: <Settings />,
},
});
}
}
const adminItems: SidebarItem[] = [
{
type: "list",
content: {
title: "Agents",
url: "agents",
icon: <ShieldHalf />,
},
},
{
type: "list",
content: {
title: "Administration panel",
url: "admin",
icon: <Settings />,
},
},
];
return (
activeOrganization && (
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu items={appItems} baseUrl={`/dashboard/${activeOrganization.slug}`} />
</SidebarGroupContent>
</SidebarGroup>
{/*(user.role === "superadmin" || user.role === "admin") && (
<SidebarGroup>
<SidebarGroupLabel>Administration</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu items={adminItems} baseUrl={`/dashboard/${organization.slug}`} />
</SidebarGroupContent>
</SidebarGroup>
)*/}
</SidebarContent>
)
);
};
@@ -1,54 +1,37 @@
"use client"
import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from "@/components/ui/chart";
import {CartesianGrid, Line, LineChart, XAxis, YAxis} from "recharts";
import {humanReadableDate} from "@/utils/date-formatting";
const data = [
{date: "2024-12-01", count: 1},
{date: "2024-12-02", count: 2},
{date: "2024-12-03", count: 4},
{date: "2024-12-04", count: 8},
{date: "2024-12-05", count: 9},
{date: "2024-12-06", count: 9},
{date: "2024-12-07", count: 10},
{date: "2024-12-08", count: 13},
{date: "2024-12-09", count: 15},
{date: "2024-12-10", count: 18},
]
"use client";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import { humanReadableDate } from "@/utils/date-formatting";
type Data = {
createdAt: Date;
}
};
export type evolutionLineChartProps = {
data: Data[]
}
data: Data[];
};
export function EvolutionLineChart(props: evolutionLineChartProps) {
const {data} = props
console.log("aaa data", data)
const { data } = props;
// Process data to calculate cumulative count
const cumulativeData = data.reduce((acc, backup) => {
const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
const cumulativeData = data.reduce(
(acc, backup) => {
const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
// Increment count for the current date or initialize it
if (acc.length && acc[acc.length - 1].date === date) {
acc[acc.length - 1].count += 1;
} else {
const lastCount = acc.length ? acc[acc.length - 1].count : 0;
acc.push({date, count: lastCount + 1});
}
return acc;
}, [] as { date: string; count: number }[]);
// Increment count for the current date or initialize it
if (acc.length && acc[acc.length - 1].date === date) {
acc[acc.length - 1].count += 1;
} else {
const lastCount = acc.length ? acc[acc.length - 1].count : 0;
acc.push({ date, count: lastCount + 1 });
}
return acc;
},
[] as { date: string; count: number }[]
);
const chartConfig = {
date: {
@@ -59,8 +42,7 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
label: "Number of backups",
color: "#60a5fa",
},
} satisfies ChartConfig
} satisfies ChartConfig;
return (
<ChartContainer config={chartConfig}>
@@ -72,27 +54,18 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
right: 12,
}}
>
<CartesianGrid vertical={false}/>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => humanReadableDate(Date(value)).split(' ')[0]}
/>
<YAxis/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel/>}
/>
<Line
dataKey="count"
type="linear"
stroke="var(--color-desktop)"
strokeWidth={2}
dot={false}
tickFormatter={(value) => humanReadableDate(new Date(value)).split(" ")[0]}
/>
<YAxis />
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Line dataKey="count" type="linear" stroke="var(--color-desktop)" strokeWidth={2} dot={false} />
</LineChart>
</ChartContainer>
)
}
);
}
@@ -1,22 +1,21 @@
"use client"
"use client";
import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from "@/components/ui/chart";
import {CartesianGrid, Line, LineChart, XAxis, YAxis} from "recharts";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { EStatusSchema } from "@/db/schema/types";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
type Data = {
createdAt: Date;
status: "success" | "failed";
_count: { id: number };
}
status: EStatusSchema;
_count: number;
};
export type percentageLineChartProps = {
data: Data[]
}
data: Data[];
};
export function PercentageLineChart(props: percentageLineChartProps) {
const {data} = props
const { data } = props;
const chartConfig = {
date: {
@@ -27,21 +26,24 @@ export function PercentageLineChart(props: percentageLineChartProps) {
label: "Success Rate",
color: "#60a5fa",
},
} satisfies ChartConfig
} satisfies ChartConfig;
const dailyStats = data.reduce((acc, backup) => {
const date = backup.createdAt.toISOString().split("T")[0]; // Format YYYY-MM-DD
const status = backup.status;
const dailyStats = data.reduce(
(acc, backup) => {
const date = backup.createdAt.toISOString().split("T")[0]; // Format YYYY-MM-DD
const status = backup.status;
if (!acc[date]) {
acc[date] = {success: 0, failed: 0, total: 0};
}
if (!acc[date]) {
acc[date] = { success: 0, failed: 0, total: 0 };
}
acc[date][status === "success" ? "success" : "failed"] += backup._count.id;
acc[date].total += backup._count.id;
acc[date][status === "success" ? "success" : "failed"] += backup._count.id;
acc[date].total += backup._count.id;
return acc;
}, {} as Record<string, { success: number; failed: number; total: number }>);
return acc;
},
{} as Record<string, { success: number; failed: number; total: number }>
);
// Format data for the chart
const formattedData = Object.entries(dailyStats).map(([date, stats]) => ({
@@ -51,35 +53,26 @@ export function PercentageLineChart(props: percentageLineChartProps) {
return (
<ChartContainer config={chartConfig}>
<LineChart
accessibilityLayer
data={formattedData}
>
<CartesianGrid strokeDasharray="3 3"/>
<XAxis dataKey="date"/>
<YAxis domain={[0, 100]} tickFormatter={(tick) => `${tick}%`}/>
<LineChart accessibilityLayer data={formattedData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis domain={[0, 100]} tickFormatter={(tick) => `${tick}%`} />
<ChartTooltip
content={<ChartTooltipContent/>}
content={<ChartTooltipContent />}
cursor={false}
defaultIndex={1}
formatter={(value, name) => (
<div className="flex min-w-[130px] items-center text-xs text-muted-foreground">
{chartConfig[name as keyof typeof chartConfig]?.label ||
name}
<div
className="ml-auto flex items-baseline gap-0.5 font-mono font-medium tabular-nums text-foreground">
{chartConfig[name as keyof typeof chartConfig]?.label || name}
<div className="ml-auto flex items-baseline gap-0.5 font-mono font-medium tabular-nums text-foreground">
{value}
<span className="font-normal text-muted-foreground">
%
</span>
<span className="font-normal text-muted-foreground">%</span>
</div>
</div>
)}
/>
<Line type="step" dataKey="successRate" stroke="#8884d8" strokeWidth={2}/>
<Line type="step" dataKey="successRate" stroke="#8884d8" strokeWidth={2} />
</LineChart>
</ChartContainer>
)
);
}