feat(client): test mount from form

This commit is contained in:
Nicolas Meienberger
2025-09-03 21:16:44 +02:00
parent 0c0a3b8581
commit ca4bd4a619
5 changed files with 78 additions and 16 deletions

View File

@@ -1,8 +1,10 @@
import { arktypeResolver } from "@hookform/resolvers/arktype";
import { volumeConfigSchema } from "@ironmount/schemas";
import { type } from "arktype";
import { Plus } from "lucide-react";
import { CheckCircle, Loader2, Plus, XCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { testConnection } from "~/api-client";
import { slugify } from "~/lib/utils";
import { Button } from "./ui/button";
import {
@@ -17,6 +19,9 @@ import {
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";
import { useMutation } from "@tanstack/react-query";
import { testConnectionMutation } from "~/api-client/@tanstack/react-query.gen";
import { toast } from "sonner";
export const formSchema = type({
name: "2<=string<=32",
@@ -30,6 +35,9 @@ type Props = {
};
export const CreateVolumeDialog = ({ open, setOpen, onSubmit }: Props) => {
const [testStatus, setTestStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const [testMessage, setTestMessage] = useState<string>("");
const form = useForm<typeof formSchema.infer>({
resolver: arktypeResolver(formSchema),
defaultValues: {
@@ -38,8 +46,36 @@ export const CreateVolumeDialog = ({ open, setOpen, onSubmit }: Props) => {
},
});
const testBackendConnection = useMutation({
...testConnectionMutation(),
onMutate: () => {
setTestStatus("loading");
},
onError: () => {
setTestStatus("error");
setTestMessage("Failed to test connection. Please try again.");
},
onSuccess: (data) => {
if (data?.success) {
setTestStatus("success");
setTestMessage(data.message);
} else {
setTestStatus("error");
setTestMessage(data?.message || "Connection test failed");
}
},
});
const watchedBackend = form.watch("backend");
const handleTestConnection = async () => {
const formValues = form.getValues();
testBackendConnection.mutate({
body: { config: formValues },
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -165,11 +201,40 @@ export const CreateVolumeDialog = ({ open, setOpen, onSubmit }: Props) => {
/>
</>
)}
{/* {createVolume.error && ( */}
{/* <div className="text-red-500 text-sm"> */}
{/* {createVolume.error.message} */}
{/* </div> */}
{/* )} */}
{watchedBackend === "nfs" && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
disabled={testStatus === "loading" || !form.watch("server") || !form.watch("exportPath")}
className="flex-1"
>
{testStatus === "loading" && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{testStatus === "success" && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
{testStatus === "error" && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
{testStatus === "idle" && "Test Connection"}
{testStatus === "loading" && "Testing..."}
{testStatus === "success" && "Connection Successful"}
{testStatus === "error" && "Test Failed"}
</Button>
</div>
{testMessage && (
<div
className={`text-sm p-2 rounded-md ${
testStatus === "success"
? "bg-green-50 text-green-700 border border-green-200"
: testStatus === "error"
? "bg-red-50 text-red-700 border border-red-200"
: "bg-gray-50 text-gray-700 border border-gray-200"
}`}
>
{testMessage}
</div>
)}
</div>
)}
<DialogFooter>
<Button variant="secondary" onClick={() => setOpen(false)}>
Cancel

View File

@@ -101,7 +101,7 @@ export default function Home({ loaderData, actionData }: Route.ComponentProps) {
<main className="relative flex flex-col pt-16 p-4 container mx-auto">
<h1 className="text-3xl font-bold mb-0 uppercase">Ironmount</h1>
<h2 className="text-sm font-semibold mb-2 text-muted-foreground">
Create, manage, monitor, and automate your Docker volumes with ease.
Create, manage, monitor, and automate your volumes with ease.
</h2>
<div className="flex items-center gap-2 mt-4 justify-between">
<span className="flex items-center gap-2">
@@ -134,7 +134,7 @@ export default function Home({ loaderData, actionData }: Route.ComponentProps) {
/>
</div>
<Table className="mt-4 border bg-white dark:bg-secondary">
<TableCaption>A list of your managed Docker volumes.</TableCaption>
<TableCaption>A list of your managed volumes.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] uppercase">Name</TableHead>

View File

@@ -13,11 +13,9 @@ export const generalDescriptor = (app: Hono) =>
info: {
title: "Ironmount API",
version: "1.0.0",
description: "API for managing Docker volumes",
description: "API for managing volumes",
},
servers: [
{ url: "http://localhost:3000", description: "Development Server" },
],
servers: [{ url: "http://localhost:3000", description: "Development Server" }],
},
});
@@ -58,9 +56,7 @@ const socketPath = "/run/docker/plugins/ironmount.sock";
fetch: app.fetch,
});
console.log(
`Server is running at http://localhost:8080 and unix socket at ${socketPath}`,
);
console.log(`Server is running at http://localhost:8080 and unix socket at ${socketPath}`);
})();
export type AppType = typeof app;

View File

@@ -19,6 +19,7 @@ const mount = async (config: BackendConfig, path: string) => {
return new Promise<void>((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
console.log("Mount command executed:", { cmd, error, stdout, stderr });
if (error) {
console.error(`Error mounting NFS volume: ${stderr}`);
return reject(new Error(`Failed to mount NFS volume: ${stderr}`));

View File

@@ -12,7 +12,7 @@ export const nfsConfigSchema = type({
backend: "'nfs'",
server: "string",
exportPath: "string",
port: "number >= 1",
port: type("string.integer.parse").or(type("number")).to("1 <= number <= 65536").default("2049"),
version: "'3' | '4' | '4.1'",
});