mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat(client): test mount from form
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||||
import { volumeConfigSchema } from "@ironmount/schemas";
|
import { volumeConfigSchema } from "@ironmount/schemas";
|
||||||
import { type } from "arktype";
|
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 { useForm } from "react-hook-form";
|
||||||
|
import { testConnection } from "~/api-client";
|
||||||
import { slugify } from "~/lib/utils";
|
import { slugify } from "~/lib/utils";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +19,9 @@ import {
|
|||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
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({
|
export const formSchema = type({
|
||||||
name: "2<=string<=32",
|
name: "2<=string<=32",
|
||||||
@@ -30,6 +35,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CreateVolumeDialog = ({ open, setOpen, onSubmit }: 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>({
|
const form = useForm<typeof formSchema.infer>({
|
||||||
resolver: arktypeResolver(formSchema),
|
resolver: arktypeResolver(formSchema),
|
||||||
defaultValues: {
|
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 watchedBackend = form.watch("backend");
|
||||||
|
|
||||||
|
const handleTestConnection = async () => {
|
||||||
|
const formValues = form.getValues();
|
||||||
|
|
||||||
|
testBackendConnection.mutate({
|
||||||
|
body: { config: formValues },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -165,11 +201,40 @@ export const CreateVolumeDialog = ({ open, setOpen, onSubmit }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* {createVolume.error && ( */}
|
{watchedBackend === "nfs" && (
|
||||||
{/* <div className="text-red-500 text-sm"> */}
|
<div className="space-y-3">
|
||||||
{/* {createVolume.error.message} */}
|
<div className="flex items-center gap-2">
|
||||||
{/* </div> */}
|
<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>
|
<DialogFooter>
|
||||||
<Button variant="secondary" onClick={() => setOpen(false)}>
|
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -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">
|
<main className="relative flex flex-col pt-16 p-4 container mx-auto">
|
||||||
<h1 className="text-3xl font-bold mb-0 uppercase">Ironmount</h1>
|
<h1 className="text-3xl font-bold mb-0 uppercase">Ironmount</h1>
|
||||||
<h2 className="text-sm font-semibold mb-2 text-muted-foreground">
|
<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>
|
</h2>
|
||||||
<div className="flex items-center gap-2 mt-4 justify-between">
|
<div className="flex items-center gap-2 mt-4 justify-between">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
@@ -134,7 +134,7 @@ export default function Home({ loaderData, actionData }: Route.ComponentProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Table className="mt-4 border bg-white dark:bg-secondary">
|
<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>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ export const generalDescriptor = (app: Hono) =>
|
|||||||
info: {
|
info: {
|
||||||
title: "Ironmount API",
|
title: "Ironmount API",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "API for managing Docker volumes",
|
description: "API for managing volumes",
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [{ url: "http://localhost:3000", description: "Development Server" }],
|
||||||
{ url: "http://localhost:3000", description: "Development Server" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,9 +56,7 @@ const socketPath = "/run/docker/plugins/ironmount.sock";
|
|||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(`Server is running at http://localhost:8080 and unix socket at ${socketPath}`);
|
||||||
`Server is running at http://localhost:8080 and unix socket at ${socketPath}`,
|
|
||||||
);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export type AppType = typeof app;
|
export type AppType = typeof app;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
exec(cmd, (error, stdout, stderr) => {
|
||||||
|
console.log("Mount command executed:", { cmd, error, stdout, stderr });
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`Error mounting NFS volume: ${stderr}`);
|
console.error(`Error mounting NFS volume: ${stderr}`);
|
||||||
return reject(new Error(`Failed to mount NFS volume: ${stderr}`));
|
return reject(new Error(`Failed to mount NFS volume: ${stderr}`));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const nfsConfigSchema = type({
|
|||||||
backend: "'nfs'",
|
backend: "'nfs'",
|
||||||
server: "string",
|
server: "string",
|
||||||
exportPath: "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'",
|
version: "'3' | '4' | '4.1'",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user