mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: volume details
This commit is contained in:
@@ -3,7 +3,7 @@ import { volumeConfigSchema } from "@ironmount/schemas";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { type } from "arktype";
|
||||
import { CheckCircle, Loader2, XCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { testConnectionMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { slugify } from "~/lib/utils";
|
||||
@@ -28,21 +28,9 @@ type Props = {
|
||||
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId }: Props) => {
|
||||
const form = useForm<FormValues>({
|
||||
resolver: arktypeResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
backend: "directory",
|
||||
},
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
const { setValue, formState, watch, getValues } = form;
|
||||
const { isDirty } = formState;
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues && !isDirty) {
|
||||
for (const [key, value] of Object.entries(initialValues)) {
|
||||
setValue(key as keyof FormValues, value as string);
|
||||
}
|
||||
}
|
||||
}, [initialValues, isDirty, setValue]);
|
||||
const { watch, getValues } = form;
|
||||
|
||||
const watchedBackend = watch("backend");
|
||||
|
||||
@@ -108,7 +96,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Backend</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a backend" />
|
||||
|
||||
20
apps/client/app/components/layout.tsx
Normal file
20
apps/client/app/components/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Outlet } from "react-router";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0",
|
||||
"[background-size:40px_40px]",
|
||||
"[background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
|
||||
"dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
|
||||
)}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black"></div>
|
||||
<main className="relative flex flex-col pt-16 p-4 container mx-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
apps/client/app/components/ui/switch.tsx
Normal file
29
apps/client/app/components/ui/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ScanHeartIcon } from "lucide-react";
|
||||
import type { GetVolumeResponse } from "~/api-client";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card } from "~/components/ui/card";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
|
||||
type Props = {
|
||||
volume: GetVolumeResponse;
|
||||
};
|
||||
|
||||
export const HealthchecksCard = ({ volume }: Props) => {
|
||||
return (
|
||||
<Card className="p-6 flex-1 flex flex-col">
|
||||
<div className="flex flex-col flex-1 justify-start">
|
||||
<span className="flex items-center gap-2 mb-4">
|
||||
<ScanHeartIcon size={24} />
|
||||
<h2 className="text-lg font-medium">Health Checks</h2>
|
||||
</span>
|
||||
<span className="">Status: {volume.status ?? "Unknown"}</span>
|
||||
<span className="text-sm text-muted-foreground mb-4">
|
||||
Last checked: {new Date(volume.lastHealthCheck).toLocaleString()}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
Enable auto remount
|
||||
<Switch className="ml-auto cursor-pointer" checked={volume.autoRemount} />
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="outline">Run Health Check</Button>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { index, type RouteConfig } from "@react-router/dev/routes";
|
||||
import { index, layout, type RouteConfig, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [index("routes/home.tsx")] satisfies RouteConfig;
|
||||
export default [
|
||||
layout("./components/layout.tsx", [index("./routes/home.tsx"), route("volumes/:name", "./routes/details.tsx")]),
|
||||
] satisfies RouteConfig;
|
||||
|
||||
89
apps/client/app/routes/details.tsx
Normal file
89
apps/client/app/routes/details.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { WifiIcon } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { getVolume } from "~/api-client";
|
||||
import { deleteVolumeMutation, getVolumeOptions } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { CreateVolumeForm } from "~/components/create-volume-form";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card } from "~/components/ui/card";
|
||||
import { VolumeIcon } from "~/components/volume-icon";
|
||||
import { parseError } from "~/lib/errors";
|
||||
import { HealthchecksCard } from "~/modules/details/components/healthchecks-card";
|
||||
import type { Route } from "./+types/details";
|
||||
|
||||
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
|
||||
const volume = await getVolume({ path: { name: params.name ?? "" } });
|
||||
if (volume.data) return volume.data;
|
||||
};
|
||||
|
||||
export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data } = useQuery({
|
||||
...getVolumeOptions({ path: { name: name ?? "" } }),
|
||||
initialData: loaderData,
|
||||
});
|
||||
|
||||
const deleteVol = useMutation({
|
||||
...deleteVolumeMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Volume deleted successfully");
|
||||
navigate("/");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to delete volume", {
|
||||
description: parseError(error)?.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteConfirm = (name: string) => {
|
||||
if (confirm(`Are you sure you want to delete the volume "${name}"? This action cannot be undone.`)) {
|
||||
deleteVol.mutate({ path: { name } });
|
||||
}
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
return <div>Volume not found</div>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-0 uppercase">Volume: {name}</h1>
|
||||
<div className="text-sm font-semibold mb-2 text-muted-foreground flex items-center gap-2">
|
||||
<span className="text-green-500 flex items-center gap-2">
|
||||
<WifiIcon size={16} />
|
||||
{data.status}
|
||||
</span>
|
||||
<VolumeIcon size={14} backend={data?.config.backend} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button>Mount</Button>
|
||||
<Button variant="destructive" onClick={() => handleDeleteConfirm(name)} disabled={deleteVol.isPending}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Card className="my-4 p-6 flex-1">
|
||||
<CreateVolumeForm initialValues={{ ...data, ...data?.config }} onSubmit={console.log} />
|
||||
</Card>
|
||||
<div className="flex flex-col my-4 gap-4">
|
||||
<HealthchecksCard volume={data} />
|
||||
<Card className="p-6 flex-1">
|
||||
<h2 className="text-lg font-medium">Volume Information</h2>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useNavigate } from "react-router";
|
||||
import { type ListVolumesResponse, listVolumes } from "~/api-client";
|
||||
import { deleteVolumeMutation, listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
|
||||
import { EditVolumeDialog } from "~/components/edit-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 { VolumeIcon } from "~/components/volume-icon";
|
||||
import { parseError } from "~/lib/errors";
|
||||
import { cn } from "~/lib/utils";
|
||||
import type { Route } from "./+types/home";
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
@@ -35,23 +32,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
const [volumeToEdit, setVolumeToEdit] = useState<ListVolumesResponse["volumes"][number]>();
|
||||
const [createVolumeOpen, setCreateVolumeOpen] = useState(false);
|
||||
|
||||
const deleteVol = useMutation({
|
||||
...deleteVolumeMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Volume deleted successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to delete volume", {
|
||||
description: parseError(error)?.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteConfirm = (name: string) => {
|
||||
if (confirm(`Are you sure you want to delete the volume "${name}"? This action cannot be undone.`)) {
|
||||
deleteVol.mutate({ path: { name } });
|
||||
}
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data } = useQuery({
|
||||
...listVolumesOptions(),
|
||||
@@ -59,103 +40,81 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0",
|
||||
"[background-size:40px_40px]",
|
||||
"[background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
|
||||
"dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
|
||||
)}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black"></div>
|
||||
<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 volumes with ease.
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 mt-4 justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Input className="w-[180px]" placeholder="Search volumes…" />
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="All status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mounted">Mounted</SelectItem>
|
||||
<SelectItem value="unmounted">Unmounted</SelectItem>
|
||||
<SelectItem value="error">Error</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="All backends" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="directory">Directory</SelectItem>
|
||||
<SelectItem value="nfs">NFS</SelectItem>
|
||||
<SelectItem value="smb">SMB</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</span>
|
||||
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
||||
</div>
|
||||
<Table className="mt-4 border bg-white dark:bg-secondary">
|
||||
<TableCaption>A list of your managed volumes.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
||||
<TableHead className="uppercase text-left">Backend</TableHead>
|
||||
<TableHead className="uppercase">Mountpoint</TableHead>
|
||||
<TableHead className="uppercase text-center">Status</TableHead>
|
||||
<TableHead className="uppercase text-right">Actions</TableHead>
|
||||
<>
|
||||
<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 volumes with ease.
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 mt-4 justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Input className="w-[180px]" placeholder="Search volumes…" />
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="All status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mounted">Mounted</SelectItem>
|
||||
<SelectItem value="unmounted">Unmounted</SelectItem>
|
||||
<SelectItem value="error">Error</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="All backends" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="directory">Directory</SelectItem>
|
||||
<SelectItem value="nfs">NFS</SelectItem>
|
||||
<SelectItem value="smb">SMB</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</span>
|
||||
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
||||
</div>
|
||||
<Table className="mt-4 border bg-white dark:bg-secondary">
|
||||
<TableCaption>A list of your managed volumes.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px] uppercase">Name</TableHead>
|
||||
<TableHead className="uppercase text-left">Backend</TableHead>
|
||||
<TableHead className="uppercase">Mountpoint</TableHead>
|
||||
<TableHead className="uppercase text-center">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.volumes.map((volume) => (
|
||||
<TableRow
|
||||
key={volume.name}
|
||||
className="hover:bg-accent/50 hover:cursor-pointer"
|
||||
onClick={() => navigate(`/volumes/${volume.name}`)}
|
||||
>
|
||||
<TableCell className="font-medium">{volume.name}</TableCell>
|
||||
<TableCell>
|
||||
<VolumeIcon backend={volume.type} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
|
||||
{volume.path}
|
||||
</span>
|
||||
<Copy size={10} />
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className="relative flex size-3 mx-auto">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
|
||||
<span className="relative inline-flex size-3 rounded-full bg-green-500" />
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.volumes.map((volume) => (
|
||||
<TableRow key={volume.name}>
|
||||
<TableCell className="font-medium">{volume.name}</TableCell>
|
||||
<TableCell>
|
||||
<VolumeIcon backend={volume.type} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
|
||||
{volume.path}
|
||||
</span>
|
||||
<Copy size={10} />
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className="relative flex size-3 mx-auto">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
|
||||
<span className="relative inline-flex size-3 rounded-full bg-green-500" />
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setVolumeToEdit(volume);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button type="button" variant="destructive" onClick={() => handleDeleteConfirm(volume.name)}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<EditVolumeDialog
|
||||
open={Boolean(volumeToEdit)}
|
||||
setOpen={() => setVolumeToEdit(undefined)}
|
||||
initialValues={volumeToEdit}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<EditVolumeDialog
|
||||
open={Boolean(volumeToEdit)}
|
||||
setOpen={() => setVolumeToEdit(undefined)}
|
||||
initialValues={volumeToEdit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@react-router/node": "^7.7.1",
|
||||
"@react-router/serve": "^7.7.1",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
|
||||
@@ -22,10 +22,16 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
const cmd = `mount -t nfs -o ${options.join(",")} ${source} ${path}`;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Mount command timed out"));
|
||||
}, 5000);
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
console.log("Mount command executed:", { cmd, error, stdout, stderr });
|
||||
clearTimeout(timeout);
|
||||
|
||||
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}`));
|
||||
}
|
||||
console.log(`NFS volume mounted successfully: ${stdout}`);
|
||||
@@ -43,8 +49,14 @@ const unmount = async (path: string) => {
|
||||
const cmd = `umount -f ${path}`;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Mount command timed out"));
|
||||
}, 5000);
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
console.log("Unmount command executed:", { cmd, error, stdout, stderr });
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (error) {
|
||||
console.error(`Error unmounting NFS volume: ${stderr}`);
|
||||
return reject(new Error(`Failed to unmount NFS volume: ${stderr}`));
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
testConnectionDto,
|
||||
updateVolumeBody,
|
||||
updateVolumeDto,
|
||||
type VolumeDto,
|
||||
} from "./volume.dto";
|
||||
import { volumeService } from "./volume.service";
|
||||
|
||||
@@ -68,13 +69,11 @@ export const volumeController = new Hono()
|
||||
}
|
||||
|
||||
const response = {
|
||||
name: res.volume.name,
|
||||
path: res.volume.path,
|
||||
type: res.volume.type,
|
||||
...res.volume,
|
||||
createdAt: res.volume.createdAt.getTime(),
|
||||
updatedAt: res.volume.updatedAt.getTime(),
|
||||
config: res.volume.config,
|
||||
};
|
||||
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
|
||||
} satisfies VolumeDto;
|
||||
|
||||
return c.json(response, 200);
|
||||
})
|
||||
|
||||
@@ -15,6 +15,8 @@ const volumeSchema = type({
|
||||
config: volumeConfigSchema,
|
||||
});
|
||||
|
||||
export type VolumeDto = typeof volumeSchema.infer;
|
||||
|
||||
/**
|
||||
* List all volumes
|
||||
*/
|
||||
|
||||
@@ -108,25 +108,17 @@ const updateVolume = async (name: string, backendConfig: BackendConfig) => {
|
||||
return { error: new NotFoundError("Volume not found") };
|
||||
}
|
||||
|
||||
const oldBackend = createVolumeBackend(existing);
|
||||
await oldBackend.unmount().catch((err) => {
|
||||
console.warn("Failed to unmount backend:", err);
|
||||
});
|
||||
|
||||
const updated = await db
|
||||
.update(volumesTable)
|
||||
.set({
|
||||
config: backendConfig,
|
||||
type: backendConfig.backend,
|
||||
updatedAt: new Date(),
|
||||
status: "unmounted",
|
||||
})
|
||||
.where(eq(volumesTable.name, name))
|
||||
.returning();
|
||||
|
||||
// Mount with new configuration
|
||||
const newBackend = createVolumeBackend(updated[0]);
|
||||
await newBackend.mount();
|
||||
|
||||
return { volume: updated[0] };
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
3
bun.lock
3
bun.lock
@@ -18,6 +18,7 @@
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@react-router/node": "^7.7.1",
|
||||
"@react-router/serve": "^7.7.1",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
@@ -287,6 +288,8 @@
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
||||
|
||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||
|
||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
Reference in New Issue
Block a user