diff --git a/apps/client/app/components/create-volume-dialog.tsx b/apps/client/app/components/create-volume-dialog.tsx index 33f3343..c88d7cc 100644 --- a/apps/client/app/components/create-volume-dialog.tsx +++ b/apps/client/app/components/create-volume-dialog.tsx @@ -1,7 +1,7 @@ -import { zodResolver } from "@hookform/resolvers/zod"; +import { arktypeResolver } from "@hookform/resolvers/arktype"; +import { type } from "arktype"; import { Plus } from "lucide-react"; import { useForm } from "react-hook-form"; -import { z } from "zod"; import { slugify } from "~/lib/utils"; import { Button } from "./ui/button"; import { @@ -15,19 +15,21 @@ import { } from "./ui/dialog"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; import { Input } from "./ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; -const formSchema = z.object({ - name: z - .string() - .min(2, { - message: "Volume name must be at least 2 characters long", - }) - .max(32, { - message: "Volume name must be at most 32 characters long", - }), +export const formSchema = type({ + name: "2<=string<=32", + backend: "'directory'", +}).or({ + name: "2<=string<=32", + backend: "'nfs'", + server: "string", + exportPath: "string", + port: "number >= 1", + version: "'3' | '4' | '4.1'", }); -type FormValues = z.infer; +type FormValues = typeof formSchema.infer; type Props = { open: boolean; setOpen: (open: boolean) => void; @@ -35,13 +37,16 @@ type Props = { }; export const CreateVolumeDialog = ({ open, setOpen, onSubmit }: Props) => { - const form = useForm>({ - resolver: zodResolver(formSchema), + const form = useForm({ + resolver: arktypeResolver(formSchema), defaultValues: { name: "", + backend: "directory", }, }); + const watchedBackend = form.watch("backend"); + return ( @@ -76,6 +81,97 @@ export const CreateVolumeDialog = ({ open, setOpen, onSubmit }: Props) => { )} /> + ( + + Backend + + Choose the storage backend for this volume. + + + )} + /> + {watchedBackend === "nfs" && ( + <> + ( + + Server + + + + NFS server IP address or hostname. + + + )} + /> + ( + + Export Path + + + + Path to the NFS export on the server. + + + )} + /> + ( + + Port + + field.onChange(parseInt(e.target.value, 10) || undefined)} + /> + + NFS server port (default: 2049). + + + )} + /> + ( + + Version + + NFS protocol version to use. + + + )} + /> + + )} {/* {createVolume.error && ( */} {/*
*/} {/* {createVolume.error.message} */} diff --git a/apps/client/app/routes/home.tsx b/apps/client/app/routes/home.tsx index d435a6b..eb1e17c 100644 --- a/apps/client/app/routes/home.tsx +++ b/apps/client/app/routes/home.tsx @@ -1,16 +1,17 @@ +import { type } from "arktype"; import { Copy, Folder } from "lucide-react"; import { useState } from "react"; import { useFetcher } from "react-router"; +import { toast } from "sonner"; import { createVolume, deleteVolume, listVolumes } from "~/api-client"; -import { CreateVolumeDialog } from "~/components/create-volume-dialog"; +import { CreateVolumeDialog, formSchema } from "~/components/create-volume-dialog"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"; +import { parseError } from "~/lib/errors"; import { cn } from "~/lib/utils"; import type { Route } from "./+types/home"; -import { parseError } from "~/lib/errors"; -import { toast } from "sonner"; export function meta(_: Route.MetaArgs) { return [ @@ -27,7 +28,8 @@ export async function clientAction({ request }: Route.ClientActionArgs) { const { _action, ...rest } = Object.fromEntries(formData.entries()); if (_action === "delete") { - const { error } = await deleteVolume({ path: { name: rest.name as string } }); + const name = rest.name as string; + const { error } = await deleteVolume({ path: { name: name } }); if (error) { toast.error("Failed to delete volume", { @@ -41,7 +43,17 @@ export async function clientAction({ request }: Route.ClientActionArgs) { } if (_action === "create") { - const { error } = await createVolume({ body: { name: rest.name as string, config: { backend: "directory" } } }); + const validationResult = formSchema(rest); + + if (validationResult instanceof type.errors) { + toast.error("Invalid form data", { + description: "Please check your input and try again.", + }); + return { error: validationResult, _action: "create" as const }; + } + + const validatedData = validationResult as typeof formSchema.infer; + const { error } = await createVolume({ body: { name: validatedData.name, config: validatedData } }); if (error) { toast.error("Failed to create volume", { description: parseError(error)?.message, @@ -139,7 +151,7 @@ export default function Home({ loaderData, actionData }: Route.ComponentProps) { - Dir + Volume diff --git a/apps/client/package.json b/apps/client/package.json index c867a41..564b905 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -31,8 +31,7 @@ "react-hook-form": "^7.62.0", "react-router": "^7.7.1", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1", - "zod": "^4.0.17" + "tailwind-merge": "^3.3.1" }, "devDependencies": { "@react-router/dev": "^7.7.1", diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index e2eafed..b198581 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -13,8 +13,8 @@ const nfsConfigSchema = type({ backend: "'nfs'", server: "string", exportPath: "string", - port: "number", - version: "string", // Shold be an enum: "3" | "4" | "4.1" + port: "number >= 1", + version: "'3' | '4' | '4.1'", }); const smbConfigSchema = type({ diff --git a/bun.lock b/bun.lock index 3796e49..e8b4ae1 100644 --- a/bun.lock +++ b/bun.lock @@ -34,7 +34,6 @@ "react-router": "^7.7.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "zod": "^4.0.17", }, "devDependencies": { "@react-router/dev": "^7.7.1", diff --git a/mutagen.yml b/mutagen.yml index c506ed6..f4031b8 100644 --- a/mutagen.yml +++ b/mutagen.yml @@ -8,10 +8,12 @@ sync: - ".DS_Store" - "tmp" - "logs" + - "mutagen.yml.lock" + - "data/ironmount.db" ironmount: alpha: "." beta: "nicolas@192.168.2.42:/home/nicolas/ironmount" - mode: "one-way-safe" + mode: "one-way-replica" flushOnCreate: true ignore: paths: