refactor: dedicated edit page for volumes (#737)

This commit is contained in:
Nico 2026-04-02 21:40:24 +02:00 committed by GitHub
parent d6e80b71d7
commit 4bf1463406
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 428 additions and 253 deletions

View file

@ -60,6 +60,7 @@ type Props = {
formId?: string; formId?: string;
loading?: boolean; loading?: boolean;
className?: string; className?: string;
readOnly?: boolean;
}; };
const defaultValuesForType = { const defaultValuesForType = {
@ -71,7 +72,15 @@ const defaultValuesForType = {
sftp: { backend: "sftp" as const, port: 22, path: "/", skipHostKeyCheck: false }, sftp: { backend: "sftp" as const, port: 22, path: "/", skipHostKeyCheck: false },
}; };
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => { export const CreateVolumeForm = ({
onSubmit,
mode = "create",
initialValues,
formId,
loading,
className,
readOnly = false,
}: Props) => {
const form = useForm<FormValues>({ const form = useForm<FormValues>({
resolver: zodResolver(formSchema, undefined, { raw: true }), resolver: zodResolver(formSchema, undefined, { raw: true }),
defaultValues: initialValues || { defaultValues: initialValues || {
@ -137,123 +146,126 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
onSubmit={form.handleSubmit(onSubmit, scrollToFirstError)} onSubmit={form.handleSubmit(onSubmit, scrollToFirstError)}
className={cn("space-y-4", className)} className={cn("space-y-4", className)}
> >
<FormField <fieldset disabled={readOnly} className="space-y-4">
control={form.control} <FormField
name="name" control={form.control}
render={({ field }) => ( name="name"
<FormItem> render={({ field }) => (
<FormLabel>Name</FormLabel> <FormItem>
<FormControl> <FormLabel>Name</FormLabel>
<Input {...field} placeholder="Volume name" maxLength={32} minLength={2} />
</FormControl>
<FormDescription>Unique identifier for the volume.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="backend"
defaultValue="directory"
render={({ field }) => (
<FormItem>
<FormLabel>Backend</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
if (mode === "create") {
form.reset({
name: form.getValues().name,
...defaultValuesForType[value as keyof typeof defaultValuesForType],
});
}
}}
value={field.value}
>
<FormControl> <FormControl>
<SelectTrigger> <Input {...field} placeholder="Volume name" maxLength={32} minLength={2} />
<SelectValue placeholder="Select a backend" />
</SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <FormDescription>Unique identifier for the volume.</FormDescription>
<SelectItem value="directory">Directory</SelectItem> <FormMessage />
<Tooltip> </FormItem>
<TooltipTrigger asChild> )}
<div> />
<SelectItem disabled={!capabilities.sysAdmin} value="nfs"> <FormField
NFS control={form.control}
</SelectItem> name="backend"
</div> defaultValue="directory"
</TooltipTrigger> render={({ field }) => (
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}> <FormItem>
<p>Remote mounts require SYS_ADMIN capability</p> <FormLabel>Backend</FormLabel>
</TooltipContent> <Select
</Tooltip> disabled={readOnly}
<Tooltip> onValueChange={(value) => {
<TooltipTrigger asChild> field.onChange(value);
<div> if (mode === "create") {
<SelectItem disabled={!capabilities.sysAdmin} value="smb"> form.reset({
SMB name: form.getValues().name,
</SelectItem> ...defaultValuesForType[value as keyof typeof defaultValuesForType],
</div> });
</TooltipTrigger> }
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}> }}
<p>Remote mounts require SYS_ADMIN capability</p> value={field.value}
</TooltipContent> >
</Tooltip> <FormControl>
<Tooltip> <SelectTrigger>
<TooltipTrigger asChild> <SelectValue placeholder="Select a backend" />
<div> </SelectTrigger>
<SelectItem disabled={!capabilities.sysAdmin} value="webdav"> </FormControl>
WebDAV <SelectContent>
</SelectItem> <SelectItem value="directory">Directory</SelectItem>
</div> <Tooltip>
</TooltipTrigger> <TooltipTrigger asChild>
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}> <div>
<p>Remote mounts require SYS_ADMIN capability</p> <SelectItem disabled={!capabilities.sysAdmin} value="nfs">
</TooltipContent> NFS
</Tooltip> </SelectItem>
<Tooltip> </div>
<TooltipTrigger asChild> </TooltipTrigger>
<div> <TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
<SelectItem disabled={!capabilities.sysAdmin} value="sftp"> <p>Remote mounts require SYS_ADMIN capability</p>
SFTP </TooltipContent>
</SelectItem> </Tooltip>
</div> <Tooltip>
</TooltipTrigger> <TooltipTrigger asChild>
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}> <div>
<p>Remote mounts require SYS_ADMIN capability</p> <SelectItem disabled={!capabilities.sysAdmin} value="smb">
</TooltipContent> SMB
</Tooltip> </SelectItem>
<Tooltip> </div>
<TooltipTrigger asChild> </TooltipTrigger>
<div> <TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
<SelectItem disabled={!capabilities.rclone || !capabilities.sysAdmin} value="rclone"> <p>Remote mounts require SYS_ADMIN capability</p>
rclone </TooltipContent>
</SelectItem> </Tooltip>
</div> <Tooltip>
</TooltipTrigger> <TooltipTrigger asChild>
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}> <div>
<p>Remote mounts require SYS_ADMIN capability</p> <SelectItem disabled={!capabilities.sysAdmin} value="webdav">
</TooltipContent> WebDAV
<TooltipContent className={cn({ hidden: !capabilities.sysAdmin || capabilities.rclone })}> </SelectItem>
<p>Setup rclone to use this backend</p> </div>
</TooltipContent> </TooltipTrigger>
</Tooltip> <TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
</SelectContent> <p>Remote mounts require SYS_ADMIN capability</p>
</Select> </TooltipContent>
<FormDescription>Choose the storage backend for this volume.</FormDescription> </Tooltip>
<FormMessage /> <Tooltip>
</FormItem> <TooltipTrigger asChild>
)} <div>
/> <SelectItem disabled={!capabilities.sysAdmin} value="sftp">
{watchedBackend === "directory" && <DirectoryForm form={form} />} SFTP
{watchedBackend === "nfs" && <NFSForm form={form} />} </SelectItem>
{watchedBackend === "webdav" && <WebDAVForm form={form} />} </div>
{watchedBackend === "smb" && <SMBForm form={form} />} </TooltipTrigger>
{watchedBackend === "rclone" && <RcloneForm form={form} />} <TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
{watchedBackend === "sftp" && <SFTPForm form={form} />} <p>Remote mounts require SYS_ADMIN capability</p>
{watchedBackend && watchedBackend !== "directory" && watchedBackend !== "rclone" && ( </TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div>
<SelectItem disabled={!capabilities.rclone || !capabilities.sysAdmin} value="rclone">
rclone
</SelectItem>
</div>
</TooltipTrigger>
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
<p>Remote mounts require SYS_ADMIN capability</p>
</TooltipContent>
<TooltipContent className={cn({ hidden: !capabilities.sysAdmin || capabilities.rclone })}>
<p>Setup rclone to use this backend</p>
</TooltipContent>
</Tooltip>
</SelectContent>
</Select>
<FormDescription>Choose the storage backend for this volume.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{watchedBackend === "directory" && <DirectoryForm form={form} />}
{watchedBackend === "nfs" && <NFSForm form={form} readOnly={readOnly} />}
{watchedBackend === "webdav" && <WebDAVForm form={form} />}
{watchedBackend === "smb" && <SMBForm form={form} readOnly={readOnly} />}
{watchedBackend === "rclone" && <RcloneForm form={form} readOnly={readOnly} />}
{watchedBackend === "sftp" && <SFTPForm form={form} readOnly={readOnly} />}
</fieldset>
{!readOnly && watchedBackend && watchedBackend !== "directory" && watchedBackend !== "rclone" && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@ -292,7 +304,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
)} )}
</div> </div>
)} )}
{mode === "update" && ( {!readOnly && mode === "update" && !formId && (
<Button type="submit" className="w-full" loading={loading}> <Button type="submit" className="w-full" loading={loading}>
<Save className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
Save changes Save changes

View file

@ -13,9 +13,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
type Props = { type Props = {
form: UseFormReturn<FormValues>; form: UseFormReturn<FormValues>;
readOnly?: boolean;
}; };
export const NFSForm = ({ form }: Props) => { export const NFSForm = ({ form, readOnly = false }: Props) => {
return ( return (
<> <>
<FormField <FormField
@ -73,7 +74,7 @@ export const NFSForm = ({ form }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Version</FormLabel> <FormLabel>Version</FormLabel>
<Select onValueChange={field.onChange} value={field.value}> <Select disabled={readOnly} onValueChange={field.onChange} value={field.value}>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select NFS version" /> <SelectValue placeholder="Select NFS version" />

View file

@ -18,9 +18,10 @@ import { useQuery } from "@tanstack/react-query";
type Props = { type Props = {
form: UseFormReturn<FormValues>; form: UseFormReturn<FormValues>;
readOnly?: boolean;
}; };
export const RcloneForm = ({ form }: Props) => { export const RcloneForm = ({ form, readOnly = false }: Props) => {
const { capabilities } = useSystemInfo(); const { capabilities } = useSystemInfo();
const { data: rcloneRemotes, isPending } = useQuery({ const { data: rcloneRemotes, isPending } = useQuery({
@ -28,7 +29,7 @@ export const RcloneForm = ({ form }: Props) => {
enabled: capabilities.rclone, enabled: capabilities.rclone,
}); });
if (!isPending && !rcloneRemotes?.length) { if (!readOnly && !isPending && !rcloneRemotes?.length) {
return ( return (
<Alert> <Alert>
<AlertDescription className="space-y-2"> <AlertDescription className="space-y-2">
@ -58,7 +59,7 @@ export const RcloneForm = ({ form }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Remote</FormLabel> <FormLabel>Remote</FormLabel>
<Select onValueChange={(v) => field.onChange(v)} value={field.value ?? ""}> <Select disabled={readOnly} onValueChange={(v) => field.onChange(v)} value={field.value ?? ""}>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select an rclone remote" /> <SelectValue placeholder="Select an rclone remote" />

View file

@ -15,9 +15,10 @@ import { Switch } from "../../../../components/ui/switch";
type Props = { type Props = {
form: UseFormReturn<FormValues>; form: UseFormReturn<FormValues>;
readOnly?: boolean;
}; };
export const SFTPForm = ({ form }: Props) => { export const SFTPForm = ({ form, readOnly = false }: Props) => {
const skipHostKeyCheck = useWatch({ control: form.control, name: "skipHostKeyCheck" }); const skipHostKeyCheck = useWatch({ control: form.control, name: "skipHostKeyCheck" });
return ( return (
@ -129,7 +130,7 @@ export const SFTPForm = ({ form }: Props) => {
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} /> <Switch checked={field.value} disabled={readOnly} onCheckedChange={field.onChange} />
</FormControl> </FormControl>
</FormItem> </FormItem>
)} )}

View file

@ -14,9 +14,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
type Props = { type Props = {
form: UseFormReturn<FormValues>; form: UseFormReturn<FormValues>;
readOnly?: boolean;
}; };
export const SMBForm = ({ form }: Props) => { export const SMBForm = ({ form, readOnly = false }: Props) => {
const guest = useWatch({ control: form.control, name: "guest" }); const guest = useWatch({ control: form.control, name: "guest" });
return ( return (
@ -116,7 +117,7 @@ export const SMBForm = ({ form }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>SMB Version</FormLabel> <FormLabel>SMB Version</FormLabel>
<Select onValueChange={field.onChange} value={field.value}> <Select disabled={readOnly} onValueChange={field.onChange} value={field.value}>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select SMB version" /> <SelectValue placeholder="Select SMB version" />

View file

@ -0,0 +1,136 @@
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { HardDrive, Check, Save } from "lucide-react";
import { useId, useState } from "react";
import { toast } from "sonner";
import { getVolumeOptions, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { type UpdateVolumeResponse } from "~/client/api-client/types.gen";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import { Alert, AlertDescription } from "~/client/components/ui/alert";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
import { parseError } from "~/client/lib/errors";
import { useNavigate } from "@tanstack/react-router";
import { ManagedBadge } from "~/client/components/managed-badge";
import { CreateVolumeForm, formSchema, type FormValues } from "../components/create-volume-form";
export function EditVolumePage({ volumeId }: { volumeId: string }) {
const navigate = useNavigate();
const formId = useId();
const [open, setOpen] = useState(false);
const [pendingValues, setPendingValues] = useState<FormValues | null>(null);
const { data } = useSuspenseQuery({
...getVolumeOptions({ path: { shortId: volumeId } }),
});
const { volume } = data;
const updateVolume = useMutation({
...updateVolumeMutation(),
onSuccess: (updatedVolume: UpdateVolumeResponse) => {
toast.success("Volume updated successfully");
setOpen(false);
setPendingValues(null);
void navigate({ to: `/volumes/${updatedVolume.shortId}` });
},
onError: (error) => {
toast.error("Failed to update volume", {
description: parseError(error)?.message,
});
setOpen(false);
setPendingValues(null);
},
});
const handleSubmit = (values: FormValues) => {
setPendingValues(values);
setOpen(true);
};
const confirmUpdate = () => {
if (!pendingValues) {
return;
}
const { name, ...config } = formSchema.parse(pendingValues);
updateVolume.mutate({
path: { shortId: volume.shortId },
body: { name, config },
});
};
return (
<>
<div className="container mx-auto space-y-6">
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<HardDrive className="w-5 h-5 text-primary" />
</div>
<div className="flex items-center gap-2">
<CardTitle>Edit Volume</CardTitle>
{volume.provisioningId && <ManagedBadge />}
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{updateVolume.isError && (
<Alert variant="destructive">
<AlertDescription>
<strong>Failed to update volume:</strong>
<br />
{parseError(updateVolume.error)?.message}
</AlertDescription>
</Alert>
)}
<CreateVolumeForm
mode="update"
formId={formId}
initialValues={{ ...volume, ...volume.config }}
onSubmit={handleSubmit}
loading={updateVolume.isPending}
/>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button type="button" variant="secondary" onClick={() => navigate({ to: `/volumes/${volume.shortId}` })}>
Cancel
</Button>
<Button type="submit" form={formId} loading={updateVolume.isPending}>
<Save className="h-4 w-4 mr-2" />
Save changes
</Button>
</div>
</CardContent>
</Card>
</div>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Update Volume Configuration</AlertDialogTitle>
<AlertDialogDescription>
Editing the volume will remount it with the new config immediately. This may temporarily disrupt access to
the volume. Continue?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmUpdate} disabled={updateVolume.isPending}>
<Check className="h-4 w-4" />
Update
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

View file

@ -9,7 +9,7 @@ import { useNavigate } from "@tanstack/react-router";
export function VolumeDetails({ volumeId }: { volumeId: string }) { export function VolumeDetails({ volumeId }: { volumeId: string }) {
const navigate = useNavigate(); const navigate = useNavigate();
const searchParams = useSearch({ from: "/(dashboard)/volumes/$volumeId" }); const searchParams = useSearch({ from: "/(dashboard)/volumes/$volumeId/" });
const activeTab = searchParams.tab || "info"; const activeTab = searchParams.tab || "info";

View file

@ -1,8 +1,8 @@
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Check, Plug, Trash2, Unplug } from "lucide-react"; import { Pencil, Plug, Trash2, Unplug } from "lucide-react";
import { CreateVolumeForm, formSchema, type FormValues } from "~/client/modules/volumes/components/create-volume-form"; import { CreateVolumeForm } from "~/client/modules/volumes/components/create-volume-form";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -22,9 +22,7 @@ import {
deleteVolumeMutation, deleteVolumeMutation,
mountVolumeMutation, mountVolumeMutation,
unmountVolumeMutation, unmountVolumeMutation,
updateVolumeMutation,
} from "~/client/api-client/@tanstack/react-query.gen"; } from "~/client/api-client/@tanstack/react-query.gen";
import type { UpdateVolumeResponse } from "~/client/api-client/types.gen";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { parseError } from "~/client/lib/errors"; import { parseError } from "~/client/lib/errors";
import { ManagedBadge } from "~/client/components/managed-badge"; import { ManagedBadge } from "~/client/components/managed-badge";
@ -37,24 +35,6 @@ type Props = {
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => { export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const updateMutation = useMutation({
...updateVolumeMutation(),
onSuccess: (data: UpdateVolumeResponse) => {
toast.success("Volume updated successfully");
setOpen(false);
setPendingValues(null);
if (data.name !== volume.name) {
void navigate({ to: `/volumes/${data.shortId}` });
}
},
onError: (error) => {
toast.error("Failed to update volume", { description: error.message });
setOpen(false);
setPendingValues(null);
},
});
const mountVol = useMutation({ const mountVol = useMutation({
...mountVolumeMutation(), ...mountVolumeMutation(),
onSuccess: () => { onSuccess: () => {
@ -92,81 +72,83 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
}, },
}); });
const [open, setOpen] = useState(false);
const [pendingValues, setPendingValues] = useState<FormValues | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const handleSubmit = (values: FormValues) => {
setPendingValues(values);
setOpen(true);
};
const confirmUpdate = () => {
if (pendingValues) {
const { name, ...config } = formSchema.parse(pendingValues);
updateMutation.mutate({
path: { shortId: volume.shortId },
body: { name, config },
});
}
};
const handleConfirmDelete = () => { const handleConfirmDelete = () => {
setShowDeleteConfirm(false); setShowDeleteConfirm(false);
deleteVol.mutate({ path: { shortId: volume.shortId } }); deleteVol.mutate({ path: { shortId: volume.shortId } });
}; };
const hasLastError = Boolean(volume.lastError);
return ( return (
<> <>
<div className="grid gap-4 xl:grid-cols-[minmax(0,2.3fr)_minmax(320px,1fr)]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,2.3fr)_minmax(320px,1fr)]">
<Card className="p-6 @container"> <div className="flex flex-col gap-4">
<div className="flex flex-col @xl:flex-row items-start @xl:items-center justify-between gap-4 mb-6"> <Card className="p-6 @container">
<div> <div className="flex flex-col @xl:flex-row items-start @xl:items-center justify-between gap-4 mb-6">
<div className="flex items-center gap-2"> <div>
<span className="text-lg font-semibold">Volume Configuration</span> <div className="flex items-center gap-2">
{volume.provisioningId && <ManagedBadge />} <span className="text-lg font-semibold">Volume Configuration</span>
{volume.provisioningId && <ManagedBadge />}
</div>
</div>
<div className="flex flex-col @xl:flex-row w-full @xl:w-auto gap-2">
<Button
type="button"
variant="outline"
onClick={() => navigate({ to: `/volumes/${volume.shortId}/edit` })}
>
<Pencil className="h-4 w-4 mr-2" />
Edit
</Button>
{volume.status !== "mounted" ? (
<Button
type="button"
onClick={() => mountVol.mutate({ path: { shortId: volume.shortId } })}
loading={mountVol.isPending}
>
<Plug className="h-4 w-4 mr-2" />
Mount
</Button>
) : (
<Button
type="button"
variant="secondary"
onClick={() => unmountVol.mutate({ path: { shortId: volume.shortId } })}
loading={unmountVol.isPending}
>
<Unplug className="h-4 w-4 mr-2" />
Unmount
</Button>
)}
<Button
type="button"
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteVol.isPending}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div> </div>
</div> </div>
<div className="flex flex-col @xl:flex-row w-full @xl:w-auto gap-2"> <CreateVolumeForm
{volume.status !== "mounted" ? ( initialValues={{ ...volume, ...volume.config }}
<Button onSubmit={() => {}}
type="button" mode="update"
onClick={() => mountVol.mutate({ path: { shortId: volume.shortId } })} readOnly
loading={mountVol.isPending} />
> </Card>
<Plug className="h-4 w-4 mr-2" /> {hasLastError && (
Mount <Card className="p-6">
</Button> <div className="space-y-2">
) : ( <p className="text-sm font-medium text-destructive">Last Error</p>
<Button <p className="text-sm text-muted-foreground wrap-break-word">{volume.lastError}</p>
type="button" </div>
variant="secondary" </Card>
onClick={() => unmountVol.mutate({ path: { shortId: volume.shortId } })} )}
loading={unmountVol.isPending} </div>
>
<Unplug className="h-4 w-4 mr-2" />
Unmount
</Button>
)}
<Button
type="button"
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteVol.isPending}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
<CreateVolumeForm
initialValues={{ ...volume, ...volume.config }}
onSubmit={handleSubmit}
mode="update"
loading={updateMutation.isPending}
/>
</Card>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="self-start w-full"> <div className="self-start w-full">
<HealthchecksCard volume={volume} /> <HealthchecksCard volume={volume} />
@ -176,24 +158,6 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
</div> </div>
</div> </div>
</div> </div>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Update Volume Configuration</AlertDialogTitle>
<AlertDialogDescription>
Editing the volume will remount it with the new config immediately. This may temporarily disrupt access to
the volume. Continue?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmUpdate}>
<Check className="h-4 w-4" />
Update
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}> <AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>

View file

@ -23,14 +23,15 @@ import { Route as dashboardNotificationsIndexRouteImport } from './routes/(dashb
import { Route as dashboardBackupsIndexRouteImport } from './routes/(dashboard)/backups/index' import { Route as dashboardBackupsIndexRouteImport } from './routes/(dashboard)/backups/index'
import { Route as dashboardAdminIndexRouteImport } from './routes/(dashboard)/admin/index' import { Route as dashboardAdminIndexRouteImport } from './routes/(dashboard)/admin/index'
import { Route as dashboardVolumesCreateRouteImport } from './routes/(dashboard)/volumes/create' import { Route as dashboardVolumesCreateRouteImport } from './routes/(dashboard)/volumes/create'
import { Route as dashboardVolumesVolumeIdRouteImport } from './routes/(dashboard)/volumes/$volumeId'
import { Route as dashboardRepositoriesCreateRouteImport } from './routes/(dashboard)/repositories/create' import { Route as dashboardRepositoriesCreateRouteImport } from './routes/(dashboard)/repositories/create'
import { Route as dashboardNotificationsCreateRouteImport } from './routes/(dashboard)/notifications/create' import { Route as dashboardNotificationsCreateRouteImport } from './routes/(dashboard)/notifications/create'
import { Route as dashboardNotificationsNotificationIdRouteImport } from './routes/(dashboard)/notifications/$notificationId' import { Route as dashboardNotificationsNotificationIdRouteImport } from './routes/(dashboard)/notifications/$notificationId'
import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create' import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create'
import { Route as authLoginErrorRouteImport } from './routes/(auth)/login.error' import { Route as authLoginErrorRouteImport } from './routes/(auth)/login.error'
import { Route as dashboardVolumesVolumeIdIndexRouteImport } from './routes/(dashboard)/volumes/$volumeId/index'
import { Route as dashboardRepositoriesRepositoryIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/index' import { Route as dashboardRepositoriesRepositoryIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/index'
import { Route as dashboardBackupsBackupIdIndexRouteImport } from './routes/(dashboard)/backups/$backupId/index' import { Route as dashboardBackupsBackupIdIndexRouteImport } from './routes/(dashboard)/backups/$backupId/index'
import { Route as dashboardVolumesVolumeIdEditRouteImport } from './routes/(dashboard)/volumes/$volumeId/edit'
import { Route as dashboardSettingsSsoNewRouteImport } from './routes/(dashboard)/settings/sso/new' import { Route as dashboardSettingsSsoNewRouteImport } from './routes/(dashboard)/settings/sso/new'
import { Route as dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit' import { Route as dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit'
import { Route as dashboardBackupsBackupIdEditRouteImport } from './routes/(dashboard)/backups/$backupId/edit' import { Route as dashboardBackupsBackupIdEditRouteImport } from './routes/(dashboard)/backups/$backupId/edit'
@ -108,12 +109,6 @@ const dashboardVolumesCreateRoute = dashboardVolumesCreateRouteImport.update({
path: '/volumes/create', path: '/volumes/create',
getParentRoute: () => dashboardRouteRoute, getParentRoute: () => dashboardRouteRoute,
} as any) } as any)
const dashboardVolumesVolumeIdRoute =
dashboardVolumesVolumeIdRouteImport.update({
id: '/volumes/$volumeId',
path: '/volumes/$volumeId',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesCreateRoute = const dashboardRepositoriesCreateRoute =
dashboardRepositoriesCreateRouteImport.update({ dashboardRepositoriesCreateRouteImport.update({
id: '/repositories/create', id: '/repositories/create',
@ -142,6 +137,12 @@ const authLoginErrorRoute = authLoginErrorRouteImport.update({
path: '/error', path: '/error',
getParentRoute: () => authLoginRoute, getParentRoute: () => authLoginRoute,
} as any) } as any)
const dashboardVolumesVolumeIdIndexRoute =
dashboardVolumesVolumeIdIndexRouteImport.update({
id: '/volumes/$volumeId/',
path: '/volumes/$volumeId/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesRepositoryIdIndexRoute = const dashboardRepositoriesRepositoryIdIndexRoute =
dashboardRepositoriesRepositoryIdIndexRouteImport.update({ dashboardRepositoriesRepositoryIdIndexRouteImport.update({
id: '/repositories/$repositoryId/', id: '/repositories/$repositoryId/',
@ -154,6 +155,12 @@ const dashboardBackupsBackupIdIndexRoute =
path: '/backups/$backupId/', path: '/backups/$backupId/',
getParentRoute: () => dashboardRouteRoute, getParentRoute: () => dashboardRouteRoute,
} as any) } as any)
const dashboardVolumesVolumeIdEditRoute =
dashboardVolumesVolumeIdEditRouteImport.update({
id: '/volumes/$volumeId/edit',
path: '/volumes/$volumeId/edit',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardSettingsSsoNewRoute = dashboardSettingsSsoNewRouteImport.update({ const dashboardSettingsSsoNewRoute = dashboardSettingsSsoNewRouteImport.update({
id: '/settings/sso/new', id: '/settings/sso/new',
path: '/settings/sso/new', path: '/settings/sso/new',
@ -201,7 +208,6 @@ export interface FileRoutesByFullPath {
'/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
'/notifications/create': typeof dashboardNotificationsCreateRoute '/notifications/create': typeof dashboardNotificationsCreateRoute
'/repositories/create': typeof dashboardRepositoriesCreateRoute '/repositories/create': typeof dashboardRepositoriesCreateRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/volumes/create': typeof dashboardVolumesCreateRoute '/volumes/create': typeof dashboardVolumesCreateRoute
'/admin/': typeof dashboardAdminIndexRoute '/admin/': typeof dashboardAdminIndexRoute
'/backups/': typeof dashboardBackupsIndexRoute '/backups/': typeof dashboardBackupsIndexRoute
@ -212,8 +218,10 @@ export interface FileRoutesByFullPath {
'/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute '/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute '/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
'/settings/sso/new': typeof dashboardSettingsSsoNewRoute '/settings/sso/new': typeof dashboardSettingsSsoNewRoute
'/volumes/$volumeId/edit': typeof dashboardVolumesVolumeIdEditRoute
'/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute '/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
'/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute '/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
'/volumes/$volumeId/': typeof dashboardVolumesVolumeIdIndexRoute
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute '/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute '/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute
'/repositories/$repositoryId/$snapshotId/': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute '/repositories/$repositoryId/$snapshotId/': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute
@ -229,7 +237,6 @@ export interface FileRoutesByTo {
'/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
'/notifications/create': typeof dashboardNotificationsCreateRoute '/notifications/create': typeof dashboardNotificationsCreateRoute
'/repositories/create': typeof dashboardRepositoriesCreateRoute '/repositories/create': typeof dashboardRepositoriesCreateRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/volumes/create': typeof dashboardVolumesCreateRoute '/volumes/create': typeof dashboardVolumesCreateRoute
'/admin': typeof dashboardAdminIndexRoute '/admin': typeof dashboardAdminIndexRoute
'/backups': typeof dashboardBackupsIndexRoute '/backups': typeof dashboardBackupsIndexRoute
@ -240,8 +247,10 @@ export interface FileRoutesByTo {
'/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute '/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute '/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
'/settings/sso/new': typeof dashboardSettingsSsoNewRoute '/settings/sso/new': typeof dashboardSettingsSsoNewRoute
'/volumes/$volumeId/edit': typeof dashboardVolumesVolumeIdEditRoute
'/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute '/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdIndexRoute
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute '/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute '/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute
'/repositories/$repositoryId/$snapshotId': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute '/repositories/$repositoryId/$snapshotId': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute
@ -260,7 +269,6 @@ export interface FileRoutesById {
'/(dashboard)/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/(dashboard)/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
'/(dashboard)/notifications/create': typeof dashboardNotificationsCreateRoute '/(dashboard)/notifications/create': typeof dashboardNotificationsCreateRoute
'/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute '/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/(dashboard)/volumes/create': typeof dashboardVolumesCreateRoute '/(dashboard)/volumes/create': typeof dashboardVolumesCreateRoute
'/(dashboard)/admin/': typeof dashboardAdminIndexRoute '/(dashboard)/admin/': typeof dashboardAdminIndexRoute
'/(dashboard)/backups/': typeof dashboardBackupsIndexRoute '/(dashboard)/backups/': typeof dashboardBackupsIndexRoute
@ -271,8 +279,10 @@ export interface FileRoutesById {
'/(dashboard)/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute '/(dashboard)/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute
'/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute '/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
'/(dashboard)/settings/sso/new': typeof dashboardSettingsSsoNewRoute '/(dashboard)/settings/sso/new': typeof dashboardSettingsSsoNewRoute
'/(dashboard)/volumes/$volumeId/edit': typeof dashboardVolumesVolumeIdEditRoute
'/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute '/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
'/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute '/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
'/(dashboard)/volumes/$volumeId/': typeof dashboardVolumesVolumeIdIndexRoute
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute '/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/(dashboard)/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute '/(dashboard)/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute
'/(dashboard)/repositories/$repositoryId/$snapshotId/': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute '/(dashboard)/repositories/$repositoryId/$snapshotId/': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute
@ -290,7 +300,6 @@ export interface FileRouteTypes {
| '/notifications/$notificationId' | '/notifications/$notificationId'
| '/notifications/create' | '/notifications/create'
| '/repositories/create' | '/repositories/create'
| '/volumes/$volumeId'
| '/volumes/create' | '/volumes/create'
| '/admin/' | '/admin/'
| '/backups/' | '/backups/'
@ -301,8 +310,10 @@ export interface FileRouteTypes {
| '/backups/$backupId/edit' | '/backups/$backupId/edit'
| '/repositories/$repositoryId/edit' | '/repositories/$repositoryId/edit'
| '/settings/sso/new' | '/settings/sso/new'
| '/volumes/$volumeId/edit'
| '/backups/$backupId/' | '/backups/$backupId/'
| '/repositories/$repositoryId/' | '/repositories/$repositoryId/'
| '/volumes/$volumeId/'
| '/backups/$backupId/$snapshotId/restore' | '/backups/$backupId/$snapshotId/restore'
| '/repositories/$repositoryId/$snapshotId/restore' | '/repositories/$repositoryId/$snapshotId/restore'
| '/repositories/$repositoryId/$snapshotId/' | '/repositories/$repositoryId/$snapshotId/'
@ -318,7 +329,6 @@ export interface FileRouteTypes {
| '/notifications/$notificationId' | '/notifications/$notificationId'
| '/notifications/create' | '/notifications/create'
| '/repositories/create' | '/repositories/create'
| '/volumes/$volumeId'
| '/volumes/create' | '/volumes/create'
| '/admin' | '/admin'
| '/backups' | '/backups'
@ -329,8 +339,10 @@ export interface FileRouteTypes {
| '/backups/$backupId/edit' | '/backups/$backupId/edit'
| '/repositories/$repositoryId/edit' | '/repositories/$repositoryId/edit'
| '/settings/sso/new' | '/settings/sso/new'
| '/volumes/$volumeId/edit'
| '/backups/$backupId' | '/backups/$backupId'
| '/repositories/$repositoryId' | '/repositories/$repositoryId'
| '/volumes/$volumeId'
| '/backups/$backupId/$snapshotId/restore' | '/backups/$backupId/$snapshotId/restore'
| '/repositories/$repositoryId/$snapshotId/restore' | '/repositories/$repositoryId/$snapshotId/restore'
| '/repositories/$repositoryId/$snapshotId' | '/repositories/$repositoryId/$snapshotId'
@ -348,7 +360,6 @@ export interface FileRouteTypes {
| '/(dashboard)/notifications/$notificationId' | '/(dashboard)/notifications/$notificationId'
| '/(dashboard)/notifications/create' | '/(dashboard)/notifications/create'
| '/(dashboard)/repositories/create' | '/(dashboard)/repositories/create'
| '/(dashboard)/volumes/$volumeId'
| '/(dashboard)/volumes/create' | '/(dashboard)/volumes/create'
| '/(dashboard)/admin/' | '/(dashboard)/admin/'
| '/(dashboard)/backups/' | '/(dashboard)/backups/'
@ -359,8 +370,10 @@ export interface FileRouteTypes {
| '/(dashboard)/backups/$backupId/edit' | '/(dashboard)/backups/$backupId/edit'
| '/(dashboard)/repositories/$repositoryId/edit' | '/(dashboard)/repositories/$repositoryId/edit'
| '/(dashboard)/settings/sso/new' | '/(dashboard)/settings/sso/new'
| '/(dashboard)/volumes/$volumeId/edit'
| '/(dashboard)/backups/$backupId/' | '/(dashboard)/backups/$backupId/'
| '/(dashboard)/repositories/$repositoryId/' | '/(dashboard)/repositories/$repositoryId/'
| '/(dashboard)/volumes/$volumeId/'
| '/(dashboard)/backups/$backupId/$snapshotId/restore' | '/(dashboard)/backups/$backupId/$snapshotId/restore'
| '/(dashboard)/repositories/$repositoryId/$snapshotId/restore' | '/(dashboard)/repositories/$repositoryId/$snapshotId/restore'
| '/(dashboard)/repositories/$repositoryId/$snapshotId/' | '/(dashboard)/repositories/$repositoryId/$snapshotId/'
@ -473,13 +486,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardVolumesCreateRouteImport preLoaderRoute: typeof dashboardVolumesCreateRouteImport
parentRoute: typeof dashboardRouteRoute parentRoute: typeof dashboardRouteRoute
} }
'/(dashboard)/volumes/$volumeId': {
id: '/(dashboard)/volumes/$volumeId'
path: '/volumes/$volumeId'
fullPath: '/volumes/$volumeId'
preLoaderRoute: typeof dashboardVolumesVolumeIdRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/repositories/create': { '/(dashboard)/repositories/create': {
id: '/(dashboard)/repositories/create' id: '/(dashboard)/repositories/create'
path: '/repositories/create' path: '/repositories/create'
@ -515,6 +521,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authLoginErrorRouteImport preLoaderRoute: typeof authLoginErrorRouteImport
parentRoute: typeof authLoginRoute parentRoute: typeof authLoginRoute
} }
'/(dashboard)/volumes/$volumeId/': {
id: '/(dashboard)/volumes/$volumeId/'
path: '/volumes/$volumeId'
fullPath: '/volumes/$volumeId/'
preLoaderRoute: typeof dashboardVolumesVolumeIdIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/repositories/$repositoryId/': { '/(dashboard)/repositories/$repositoryId/': {
id: '/(dashboard)/repositories/$repositoryId/' id: '/(dashboard)/repositories/$repositoryId/'
path: '/repositories/$repositoryId' path: '/repositories/$repositoryId'
@ -529,6 +542,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport
parentRoute: typeof dashboardRouteRoute parentRoute: typeof dashboardRouteRoute
} }
'/(dashboard)/volumes/$volumeId/edit': {
id: '/(dashboard)/volumes/$volumeId/edit'
path: '/volumes/$volumeId/edit'
fullPath: '/volumes/$volumeId/edit'
preLoaderRoute: typeof dashboardVolumesVolumeIdEditRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/settings/sso/new': { '/(dashboard)/settings/sso/new': {
id: '/(dashboard)/settings/sso/new' id: '/(dashboard)/settings/sso/new'
path: '/settings/sso/new' path: '/settings/sso/new'
@ -607,7 +627,6 @@ interface dashboardRouteRouteChildren {
dashboardNotificationsNotificationIdRoute: typeof dashboardNotificationsNotificationIdRoute dashboardNotificationsNotificationIdRoute: typeof dashboardNotificationsNotificationIdRoute
dashboardNotificationsCreateRoute: typeof dashboardNotificationsCreateRoute dashboardNotificationsCreateRoute: typeof dashboardNotificationsCreateRoute
dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
dashboardVolumesCreateRoute: typeof dashboardVolumesCreateRoute dashboardVolumesCreateRoute: typeof dashboardVolumesCreateRoute
dashboardAdminIndexRoute: typeof dashboardAdminIndexRoute dashboardAdminIndexRoute: typeof dashboardAdminIndexRoute
dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute
@ -618,8 +637,10 @@ interface dashboardRouteRouteChildren {
dashboardBackupsBackupIdEditRoute: typeof dashboardBackupsBackupIdEditRoute dashboardBackupsBackupIdEditRoute: typeof dashboardBackupsBackupIdEditRoute
dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute
dashboardSettingsSsoNewRoute: typeof dashboardSettingsSsoNewRoute dashboardSettingsSsoNewRoute: typeof dashboardSettingsSsoNewRoute
dashboardVolumesVolumeIdEditRoute: typeof dashboardVolumesVolumeIdEditRoute
dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute
dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute
dashboardVolumesVolumeIdIndexRoute: typeof dashboardVolumesVolumeIdIndexRoute
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute
dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute
@ -631,7 +652,6 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardNotificationsNotificationIdRoute, dashboardNotificationsNotificationIdRoute,
dashboardNotificationsCreateRoute: dashboardNotificationsCreateRoute, dashboardNotificationsCreateRoute: dashboardNotificationsCreateRoute,
dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute, dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute,
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
dashboardVolumesCreateRoute: dashboardVolumesCreateRoute, dashboardVolumesCreateRoute: dashboardVolumesCreateRoute,
dashboardAdminIndexRoute: dashboardAdminIndexRoute, dashboardAdminIndexRoute: dashboardAdminIndexRoute,
dashboardBackupsIndexRoute: dashboardBackupsIndexRoute, dashboardBackupsIndexRoute: dashboardBackupsIndexRoute,
@ -643,9 +663,11 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardRepositoriesRepositoryIdEditRoute: dashboardRepositoriesRepositoryIdEditRoute:
dashboardRepositoriesRepositoryIdEditRoute, dashboardRepositoriesRepositoryIdEditRoute,
dashboardSettingsSsoNewRoute: dashboardSettingsSsoNewRoute, dashboardSettingsSsoNewRoute: dashboardSettingsSsoNewRoute,
dashboardVolumesVolumeIdEditRoute: dashboardVolumesVolumeIdEditRoute,
dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute, dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute,
dashboardRepositoriesRepositoryIdIndexRoute: dashboardRepositoriesRepositoryIdIndexRoute:
dashboardRepositoriesRepositoryIdIndexRoute, dashboardRepositoriesRepositoryIdIndexRoute,
dashboardVolumesVolumeIdIndexRoute: dashboardVolumesVolumeIdIndexRoute,
dashboardBackupsBackupIdSnapshotIdRestoreRoute: dashboardBackupsBackupIdSnapshotIdRestoreRoute:
dashboardBackupsBackupIdSnapshotIdRestoreRoute, dashboardBackupsBackupIdSnapshotIdRestoreRoute,
dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute: dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute:

View file

@ -0,0 +1,37 @@
import { createFileRoute } from "@tanstack/react-router";
import { getVolumeOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { EditVolumePage } from "~/client/modules/volumes/routes/edit-volume";
export const Route = createFileRoute("/(dashboard)/volumes/$volumeId/edit")({
component: RouteComponent,
errorComponent: (e) => <div>{e.error.message}</div>,
loader: async ({ params, context }) => {
const volume = await context.queryClient.ensureQueryData({
...getVolumeOptions({ path: { shortId: params.volumeId } }),
});
return volume;
},
staticData: {
breadcrumb: (match) => [
{ label: "Volumes", href: "/volumes" },
{ label: match.loaderData?.volume.name || "Volume", href: `/volumes/${match.params.volumeId}` },
{ label: "Edit" },
],
},
head: ({ loaderData }) => ({
meta: [
{ title: `Zerobyte - Edit ${loaderData?.volume.name ?? "Volume"}` },
{
name: "description",
content: "Edit volume configuration.",
},
],
}),
});
function RouteComponent() {
const { volumeId } = Route.useParams();
return <EditVolumePage volumeId={volumeId} />;
}

View file

@ -3,7 +3,7 @@ import { z } from "zod";
import { getVolumeOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { getVolumeOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { VolumeDetails } from "~/client/modules/volumes/routes/volume-details"; import { VolumeDetails } from "~/client/modules/volumes/routes/volume-details";
export const Route = createFileRoute("/(dashboard)/volumes/$volumeId")({ export const Route = createFileRoute("/(dashboard)/volumes/$volumeId/")({
component: RouteComponent, component: RouteComponent,
errorComponent: (e) => <div>{e.error.message}</div>, errorComponent: (e) => <div>{e.error.message}</div>,
loader: async ({ params, context }) => { loader: async ({ params, context }) => {