Merge remote-tracking branch 'origin/main'

# Conflicts:
#	app/(customer)/dashboard/agents/[agentId]/page.tsx
This commit is contained in:
charles-gauthereau
2024-11-30 16:30:17 +01:00
25 changed files with 772 additions and 202 deletions
+10 -9
View File
@@ -6,7 +6,6 @@ import {env} from "@/env.mjs";
import GoogleProvider from "next-auth/providers/google";
export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
adapter: PrismaAdapter(prisma),
theme: {
@@ -79,20 +78,22 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
// // session.user.authMethod = user.authMethod;
// return session
// },
async jwt({ token, trigger, session, user }) {
async jwt({token, trigger, session, user}) {
if (trigger === "update" && session) {
return { ...token, ...session?.user };
return {...session, user: {...token, ...session?.user}};
// return {...token, ...session?.user};
}
return { ...token, ...user };
return {...token, ...user};
},
async session({ session, token, user }) {
session.user = token;
async session({session, token, user}) {
session.user = token.user;
// session.user = token;
return session;
},
async signIn({ account, user, profile }) {
async signIn({account, user, profile}) {
const existingUser = await prisma.user.findFirst({
where: { email: user.email },
where: {email: user.email},
});
if (!existingUser) {
@@ -125,7 +126,7 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
// Update auth method if user exists
await prisma.user.update({
where: { id: existingUser.id },
where: {id: existingUser.id},
data: {
authMethod: account.provider,
},
+11
View File
@@ -0,0 +1,11 @@
import {baseAuth} from "@/auth/auth";
import {Organization} from "@prisma/client";
export const currentOrganization = async () => {
const session = await baseAuth();
console.log("mysession", session)
if (!session?.organization) {
return null;
}
return session.organization as Organization;
}
+2
View File
@@ -3,12 +3,14 @@ import {User} from "@prisma/client";
export const currentUser = async () => {
const session = await baseAuth();
console.log("mysession", session)
if (!session?.user) {
return null;
}
return session.user as User;
}
export const requiredCurrentUser = async () => {
const user = await currentUser();
if (!user) {
@@ -71,9 +71,9 @@ export const AgentForm = (props: agentFormProps) => {
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder={"Project 1"} {...field} />
placeholder="Agent 1" {...field} />
</FormControl>
<FormDescription>{"Your agent project name"}</FormDescription>
<FormDescription>Your agent project name</FormDescription>
<FormMessage/>
</FormItem>
)}
@@ -89,14 +89,14 @@ export const AgentForm = (props: agentFormProps) => {
<FormControl>
<Input
value={field.value ?? ""}
placeholder={"agent-5-project-1"} {...field}
placeholder="agent-1" {...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
field.onChange(value)
}}
/>
</FormControl>
<FormDescription>{'The slug is used in the url of the agent'}</FormDescription>
<FormDescription>The slug is used in the url of the agent</FormDescription>
<FormMessage/>
</FormItem>
)}
@@ -111,10 +111,10 @@ export const AgentForm = (props: agentFormProps) => {
<FormLabel>Description</FormLabel>
<FormControl>
<Input
placeholder={'This agent is for the client exemple.com'} {...field}
placeholder='This agent is for the client exemple.com' {...field}
value={field.value ?? ""}/>
</FormControl>
<FormDescription>{"Enter your project agent description"}</FormDescription>
<FormDescription>Enter your project agent description</FormDescription>
<FormMessage/>
</FormItem>
)}
@@ -1,10 +1,11 @@
"use client"
import {SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem} from "@/components/ui/sidebar";
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 {PropsWithChildren, useEffect, useState} from "react";
import {ChartArea, Home, Settings, ShieldHalf} from "lucide-react";
import {ChartArea, FolderKanban, Home, Settings, ShieldHalf} from "lucide-react";
import {usePathname} from "next/navigation";
export type SidebarMenuCustomProps = {}
@@ -20,6 +21,11 @@ export const SidebarMenuCustom = (props: SidebarMenuCustomProps) => {
// url: "/",
// icon: Home,
// },
{
title: "Projects",
url: "projects",
icon: FolderKanban,
},
{
title: "Agents",
url: "agents",
@@ -4,18 +4,21 @@ import {
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {
ChevronDown
} from "lucide-react";
import {LoggedInButton} from "@/components/wrappers/Dashboard/LoggedInButton/LoggedInButton";
import {SidebarMenuCustom} from "@/components/wrappers/Dashboard/SideBar/SideBarMenu/SideBarMenu";
import Image from 'next/image';
import {OrganizationComboBox} from "@/components/wrappers/organization/OrganizationCombobox";
import {prisma} from "@/prisma";
export function AppSidebar() {
export async function AppSidebar() {
const organizations = await prisma.organization.findMany({})
const defaultOrganization = await prisma.organization.findUnique({
where: {
slug: "default"
}
})
return (
<Sidebar collapsible="icon">
@@ -25,22 +28,10 @@ export function AppSidebar() {
</div>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton>
Select Project
<ChevronDown className="ml-auto"/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[--radix-popper-anchor-width]">
<DropdownMenuItem>
<span>Project 1</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>Project 2</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<OrganizationComboBox
organizations={organizations}
defaultOrganization={defaultOrganization}
/>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
+11 -10
View File
@@ -1,8 +1,8 @@
"use client"
import {useEffect, useState} from "react";
import {useState} from "react";
import {Check, ChevronsUpDown} from "lucide-react"
import {Check, ChevronDown} from "lucide-react"
import {cn} from "@/lib/utils"
import {Button} from "@/components/ui/button"
@@ -45,15 +45,15 @@ export function ComboBox(props: comboBoxProps) {
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
className="w-full justify-between"
>
{value
? choices.find((choice) => choice.value === value)?.label
: "Select choice..."}
<ChevronsUpDown className="opacity-50"/>
<ChevronDown className="opacity-50"/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<PopoverContent className="p-0 popover-content-width-full">
<Command>
{searchField ? <CommandInput placeholder="Search choice..." className="h-9"/> : null}
<CommandList>
@@ -64,8 +64,8 @@ export function ComboBox(props: comboBoxProps) {
key={choice.value}
value={choice.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
onValueChange(currentValue === value ? "" : currentValue)
setValue(currentValue)
onValueChange(currentValue)
setOpen(false)
}}
>
@@ -93,6 +93,7 @@ export type comboBoxFormItemProps = comboBoxProps & {
onChange: any
};
/** Combobox to use when working with zodForm. */
export function ComboBoxFormItem(props: comboBoxFormItemProps) {
const {values: choices, searchField = false, value, name, onChange} = props;
@@ -108,7 +109,7 @@ export function ComboBoxFormItem(props: comboBoxFormItemProps) {
role="combobox"
aria-expanded={open}
className={cn(
"w-[200px] justify-between",
"w-full justify-between",
!value && "text-muted-foreground"
)}
>
@@ -117,11 +118,11 @@ export function ComboBoxFormItem(props: comboBoxFormItemProps) {
(choice) => choice.value === value
)?.label
: `Select ${name}`}
<ChevronsUpDown className="opacity-50"/>
<ChevronDown className="opacity-50"/>
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<PopoverContent className="p-0 popover-content-width-full">
<Command>
{searchField ? <CommandInput placeholder="Search choice..." className="h-9"/> : null}
<CommandList>
@@ -0,0 +1,36 @@
"use client"
import {useSession} from "next-auth/react";
import {ComboBox} from "@/components/wrappers/combobox";
import {Organization} from "@prisma/client";
export type organizationComboBoxProps = {
organizations: Organization[]
defaultOrganization: Organization
}
export function OrganizationComboBox(props: organizationComboBoxProps) {
const {organizations, defaultOrganization} = props
const values = organizations.map(organization => {
return ({
value: organization.id,
label: organization.name,
})
})
const {data: session, update} = useSession();
const updateSession = async (organizationId: string) => {
const organization = organizations.find(organization => organization.id === organizationId)
await update({...session, organization: organization})
console.log("session updtated", organization)
}
return (
<ComboBox values={values} defaultValue={defaultOrganization.id} onValueChange={updateSession}/>
)
}
@@ -0,0 +1,26 @@
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import Link from "next/link";
export type projectCardProps = {
data: any
}
export const ProjectCard = (props: projectCardProps) => {
const {data: project} = props;
return (
<Link href={`/dashboard/projects/${project.id}`}>
<Card className="flex flex-row justify-between">
<div className="">
<CardHeader>{project.name}</CardHeader>
<CardContent>
</CardContent>
</div>
</Card>
</Link>
)
}
@@ -0,0 +1,21 @@
"use server"
import {userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {ProjectSchema} from "@/components/wrappers/project/ProjectForm.schema";
export const createProjectAction = userAction
.schema(ProjectSchema)
.action(async ({parsedInput, ctx}) => {
// Verify if slug already exist
// await verifySlugUniqueness(parsedInput.slug);
console.log("ctx", ctx)
return prisma.project.create({
data: {
...parsedInput,
}
});
});
@@ -0,0 +1,9 @@
import {z} from "zod";
export const ProjectSchema = z.object({
name: z.string(),
slug: z.string(),
});
export type ProjectSchema = z.infer<typeof ProjectSchema>;
@@ -0,0 +1,99 @@
"use client";
import {Card, CardContent, CardHeader} 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 {useMutation} from "@tanstack/react-query";
import {ProjectSchema} from "@/components/wrappers/project/ProjectForm.schema";
import {createProjectAction} from "@/components/wrappers/project/ProjectForm.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {useSession} from "next-auth/react";
export type projectFormProps = {
defaultValues?: ProjectSchema;
}
export const ProjectForm = (props: projectFormProps) => {
const router = useRouter();
const {data: session} = useSession()
console.log("organization", session)
const form = useZodForm({
schema: ProjectSchema,
});
const mutation = useMutation({
mutationFn: async (values: ProjectSchema) => {
const {data, validationErrors} = await createProjectAction({...values, organizationId: organization?.id});
if (data) {
toast.success(`Success`);
router.push(`/dashboard/projects/${data.id}`);
router.refresh()
}
if (validationErrors) {
toast.success(`Error`);
}
}
})
return (
<Card>
<CardHeader>
</CardHeader>
<CardContent>
<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}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Project 1" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
placeholder="project-1" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Button>
Create Project
</Button>
</Form>
</CardContent>
</Card>
)
}
+7 -2
View File
@@ -1,6 +1,9 @@
import {createSafeActionClient} from "next-safe-action";
import {currentUser} from "@/auth/current-user";
import {baseAuth} from "@/auth/auth";
import {Organization} from "@prisma/client";
import {currentOrganization} from "@/auth/current-organization";
export class ActionError extends Error {
constructor(message: string) {
@@ -18,15 +21,17 @@ const handleReturnedServerError = (error: Error) => {
}
export const action = createSafeActionClient(
{
handleReturnedServerError: handleReturnedServerError
});
export const userAction = action.use(async ({ next, ctx }) => {
export const userAction = action.use(async ({next, ctx}) => {
const user = await currentUser();
if (!user) {
throw new ActionError("You must be logged in");
}
return next({ ctx: { user } });
return next({ctx: {user}});
});
+29 -4
View File
@@ -6,6 +6,10 @@ import {env} from "@/env.mjs";
export function init() {
consoleAscii()
console.log("====Init Functions====")
createDefaultOrganization().then(() => {
})
createSettingsIfNotExist()
.then(() => {
console.log('====Initialization completed====');
@@ -31,20 +35,19 @@ async function createSettingsIfNotExist() {
}
const settings = await prisma.settings.findUnique({
where: {
name: "system",
}
})
if(!settings){
if (!settings) {
console.log("====Init Setting : Create ====")
await prisma.settings.create({
data: {
...configSettings
}
})
}else{
} else {
console.log("====Init Setting : Update ====")
await prisma.settings.update({
where: {
@@ -58,7 +61,29 @@ async function createSettingsIfNotExist() {
}
function consoleAscii(){
async function createDefaultOrganization() {
const defaultOrganizationConf = {
slug: "default",
name: "Default Organization",
}
const defaultOrganization = await prisma.organization.findUnique({
where: {
slug: "default",
}
})
if (!defaultOrganization) {
console.log("==== Creating default organization... ====\n")
await prisma.organization.create({
data: {
...defaultOrganizationConf
}
})
}
}
function consoleAscii() {
console.log("\n" +
" ____ __ __ _____ \n" +
" / __ \\ ____ _____ / /_ ____ _ / /_ ____ _ _____ ___ / ___/ ___ _____ _ __ ___ _____\n" +
+122
View File
@@ -0,0 +1,122 @@
export const organizations = [
{
"id": "org-1a2b3c4e",
"slug": "default",
"name": "Default Organization",
"createdAt": "2024-01-10T10:00:00.000Z",
"projects": []
},
{
"id": "org-1a2b3c4d",
"slug": "tech-corp",
"name": "Tech Corp",
"createdAt": "2024-01-10T10:00:00.000Z",
"projects": []
},
{
"id": "org-2e3f4g5h",
"slug": "design-studio",
"name": "Design Studio",
"createdAt": "2023-09-15T15:45:00.000Z",
"projects": []
}
]
export const projects = [
{
"id": "proj-1234abcd",
"slug": "backend-system",
"name": "Backend System",
"createdAt": "2024-02-20T11:30:00.000Z",
"organizationId": "org-1a2b3c4d",
"databases": []
}, {
"id": "proj-5678efgh",
"slug": "creative-suite",
"name": "Creative Suite",
"createdAt": "2023-10-10T08:20:00.000Z",
"organizationId": "org-2e3f4g5h",
"databases": []
}
]
export const databases = [
{
"id": "c1a8e77c-eed9-4c8f-a9f7-90deabc3b78e",
"name": "Main Production DB",
"dbms": "postgresql",
"generatedId": "prod-db-1",
"description": "Primary database for production environment",
"backupPolicy": "daily",
"createdAt": "2024-11-01T12:00:00.000Z",
"agentId": "agent-1234",
"lastContact": "2024-11-28T08:30:00.000Z",
"projectId": "proj-789",
"backups": [],
"restaurations": []
},
{
"id": "1e4f9265-5f10-4bd4-8e5c-3283746cfd7a",
"name": "Staging DB",
"dbms": "mysql",
"generatedId": "staging-db-2",
"description": "Database for testing and staging environment",
"backupPolicy": "weekly",
"createdAt": "2024-10-15T09:45:00.000Z",
"agentId": "agent-5678",
"lastContact": "2024-11-25T10:15:00.000Z",
"projectId": null,
"backups": [],
"restaurations": []
},
{
"id": "b6f92bd7-834b-41c1-b6b3-dc4214b1de08",
"name": "Development DB",
"dbms": "mongodb",
"generatedId": "dev-db-3",
"description": null,
"backupPolicy": null,
"createdAt": "2024-09-20T16:20:00.000Z",
"agentId": "agent-9012",
"lastContact": null,
"projectId": "proj-456",
"backups": [],
"restaurations": []
}
]
export const backups = [
{'id': 'backup-1', 'createdAt': '2023-11-27T11:26:54.870914Z', 'status': 'pending'},
{'id': 'backup-2', 'createdAt': '2023-12-03T11:26:54.870927Z', 'status': 'failed'},
{'id': 'backup-3', 'createdAt': '2023-12-12T11:26:54.870937Z', 'status': 'success'},
{'id': 'backup-4', 'createdAt': '2023-11-23T11:26:54.870946Z', 'status': 'processing'},
{'id': 'backup-5', 'createdAt': '2023-12-09T11:26:54.870955Z', 'status': 'pending'},
{'id': 'backup-6', 'createdAt': '2023-11-29T11:26:54.870964Z', 'status': 'failed'},
{'id': 'backup-7', 'createdAt': '2023-12-07T11:26:54.870973Z', 'status': 'success'},
{'id': 'backup-8', 'createdAt': '2023-11-18T11:26:54.870982Z', 'status': 'processing'},
{'id': 'backup-9', 'createdAt': '2023-12-01T11:26:54.870991Z', 'status': 'pending'},
{'id': 'backup-10', 'createdAt': '2023-11-25T11:26:54.871000Z', 'status': 'failed'},
{'id': 'backup-1', 'createdAt': '2023-11-27T11:26:54.870914Z', 'status': 'pending'},
{'id': 'backup-2', 'createdAt': '2023-12-03T11:26:54.870927Z', 'status': 'failed'},
{'id': 'backup-3', 'createdAt': '2023-12-12T11:26:54.870937Z', 'status': 'success'},
{'id': 'backup-4', 'createdAt': '2023-11-23T11:26:54.870946Z', 'status': 'processing'},
{'id': 'backup-5', 'createdAt': '2023-12-09T11:26:54.870955Z', 'status': 'pending'},
{'id': 'backup-6', 'createdAt': '2023-11-29T11:26:54.870964Z', 'status': 'failed'},
{'id': 'backup-7', 'createdAt': '2023-12-07T11:26:54.870973Z', 'status': 'success'},
{'id': 'backup-8', 'createdAt': '2023-11-18T11:26:54.870982Z', 'status': 'processing'},
{'id': 'backup-9', 'createdAt': '2023-12-01T11:26:54.870991Z', 'status': 'pending'},
{'id': 'backup-11', 'createdAt': '2023-11-25T11:26:54.871000Z', 'status': 'failed'}
]
export const restaurations = [
{'id': 'restore-1', 'backupId': 'backup-1', 'createdAt': '2023-11-27T11:26:54.870914Z', 'status': 'pending'},
{'id': 'restore-2', 'backupId': 'backup-2', 'createdAt': '2023-12-03T11:26:54.870927Z', 'status': 'failed'},
{'id': 'restore-3', 'backupId': 'backup-3', 'createdAt': '2023-12-12T11:26:54.870937Z', 'status': 'success'},
{'id': 'restore-4', 'backupId': 'backup-4', 'createdAt': '2023-11-23T11:26:54.870946Z', 'status': 'processing'},
{'id': 'restore-5', 'backupId': 'backup-5', 'createdAt': '2023-12-09T11:26:54.870955Z', 'status': 'pending'},
{'id': 'restore-6', 'backupId': 'backup-6', 'createdAt': '2023-11-29T11:26:54.870964Z', 'status': 'failed'},
{'id': 'restore-7', 'backupId': 'backup-7', 'createdAt': '2023-12-07T11:26:54.870973Z', 'status': 'success'},
{'id': 'restore-8', 'backupId': 'backup-8', 'createdAt': '2023-11-18T11:26:54.870982Z', 'status': 'processing'},
{'id': 'restore-9', 'backupId': 'backup-9', 'createdAt': '2023-12-01T11:26:54.870991Z', 'status': 'pending'},
{'id': 'restore-10', 'backupId': 'backup-10', 'createdAt': '2023-11-25T11:26:54.871000Z', 'status': 'failed'}
]