diff --git a/app/(main)/_components/createProject.tsx b/app/(main)/_components/createProject.tsx new file mode 100644 index 0000000..95d0079 --- /dev/null +++ b/app/(main)/_components/createProject.tsx @@ -0,0 +1,308 @@ +"use client"; + +// Component for creating new Incus projects +import { Button } from "@/app/_components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/app/_components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/app/_components/ui/form"; +import { Input } from "@/app/_components/ui/input"; +import { useState, use } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import z from "zod"; +import { createProject } from "@/app/(main)/_lib/projects"; +import { mutate } from "swr"; +import { toast } from "sonner"; +import { Textarea } from "@/app/_components/textarea"; +import { useConfigurableOptions } from "@/app/_hooks/server"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/app/_components/ui/accordion"; +import ProjectsContext from "@/app/(main)/_context/projects"; +import { Checkbox } from "@/app/_components/ui/checkbox"; + +const formSchema = z.object({ + name: z + .string() + .min(1, "Project name is required") + .max(63, "Project name must be at most 63 characters long") + .regex( + /^[a-z][a-z0-9-]*[a-z0-9]$/, + "Project name must start with a lowercase letter and can only contain lowercase letters, numbers, and dashes. It cannot end with a dash." + ), + description: z.string().optional(), + config: z.record(z.string()).optional(), +}); + +interface ProjectConfigField { + key: string; + type?: string; + shortdesc?: string; + longdesc?: string; + defaultdesc?: string; + initialvaluedesc?: string; +} + +interface ConfigCategory { + keys: Array>; +} + +interface ProjectConfigOptions { + [category: string]: ConfigCategory; +} + +interface CreateProjectProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function CreateProject({ open, onOpenChange }: CreateProjectProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const { data: configurableOptions } = useConfigurableOptions(); + const { setProject } = use(ProjectsContext); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + description: "", + config: {}, + }, + }); + + // Parse project configuration options from metadata + const projectConfigOptions: Array<{ category: string; fields: Array<{ name: string; field: ProjectConfigField }> }> = []; + + if (configurableOptions?.configs?.project) { + const projectConfig = configurableOptions.configs.project as ProjectConfigOptions; + + Object.entries(projectConfig).forEach(([category, categoryData]) => { + if (categoryData.keys && Array.isArray(categoryData.keys)) { + const fields = categoryData.keys.map((keyObj) => { + const [name, field] = Object.entries(keyObj)[0]; + return { name, field }; + }); + projectConfigOptions.push({ category, fields }); + } + }); + } + + const onSubmit = async (values: z.infer) => { + try { + setIsSubmitting(true); + + // Filter out empty config values + const config = Object.entries(values.config || {}).reduce( + (acc, [key, value]) => { + if (value && value.trim() !== "") { + acc[key] = value; + } + return acc; + }, + {} as Record + ); + + await createProject({ + name: values.name, + description: values.description, + config: Object.keys(config).length > 0 ? config : undefined, + }); + + // Invalidate the projects cache to refresh the list + await mutate("/1.0/projects?recursion=1"); + + // Set the newly created project as the current project + setProject(values.name); + + onOpenChange(false); + form.reset(); + } catch (error) { + console.error("Failed to create project:", error); + toast.error("Failed to create project", { + description: + error instanceof Error ? error.message : "An unknown error occurred", + }); + } finally { + setIsSubmitting(false); + } + }; + + const renderConfigField = ( + name: string, + field: ProjectConfigField, + formField: { + value: string; + onChange: (value: string) => void; + onBlur: () => void; + name: string; + ref: React.Ref; + } + ) => { + const defaultDesc = field.defaultdesc || field.initialvaluedesc || ""; + + if (field.type === "bool") { + return ( +
+ { + formField.onChange(checked ? "true" : "false"); + }} + disabled={isSubmitting} + /> + +
+ ); + } else if (field.type === "integer") { + return ( + + ); + } else { + return ( + + ); + } + }; + + return ( + + + + Create Project + + Create a new project with optional configuration + + +
+ + ( + + Name + + + + + The name must start with a lowercase letter and contain only + lowercase letters, numbers, and dashes. + + + + )} + /> + ( + + Description (Optional) + +