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

View file

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

View file

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

View file

@ -15,9 +15,10 @@ import { Switch } from "../../../../components/ui/switch";
type Props = {
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" });
return (
@ -129,7 +130,7 @@ export const SFTPForm = ({ form }: Props) => {
</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch checked={field.value} disabled={readOnly} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}

View file

@ -14,9 +14,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
type Props = {
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" });
return (
@ -116,7 +117,7 @@ export const SMBForm = ({ form }: Props) => {
render={({ field }) => (
<FormItem>
<FormLabel>SMB Version</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<Select disabled={readOnly} onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<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 }) {
const navigate = useNavigate();
const searchParams = useSearch({ from: "/(dashboard)/volumes/$volumeId" });
const searchParams = useSearch({ from: "/(dashboard)/volumes/$volumeId/" });
const activeTab = searchParams.tab || "info";

View file

@ -1,8 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
import { Check, Plug, Trash2, Unplug } from "lucide-react";
import { CreateVolumeForm, formSchema, type FormValues } from "~/client/modules/volumes/components/create-volume-form";
import { Pencil, Plug, Trash2, Unplug } from "lucide-react";
import { CreateVolumeForm } from "~/client/modules/volumes/components/create-volume-form";
import {
AlertDialog,
AlertDialogAction,
@ -22,9 +22,7 @@ import {
deleteVolumeMutation,
mountVolumeMutation,
unmountVolumeMutation,
updateVolumeMutation,
} from "~/client/api-client/@tanstack/react-query.gen";
import type { UpdateVolumeResponse } from "~/client/api-client/types.gen";
import { useNavigate } from "@tanstack/react-router";
import { parseError } from "~/client/lib/errors";
import { ManagedBadge } from "~/client/components/managed-badge";
@ -37,24 +35,6 @@ type Props = {
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
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({
...mountVolumeMutation(),
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 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 = () => {
setShowDeleteConfirm(false);
deleteVol.mutate({ path: { shortId: volume.shortId } });
};
const hasLastError = Boolean(volume.lastError);
return (
<>
<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 @xl:flex-row items-start @xl:items-center justify-between gap-4 mb-6">
<div>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold">Volume Configuration</span>
{volume.provisioningId && <ManagedBadge />}
<div className="flex flex-col gap-4">
<Card className="p-6 @container">
<div className="flex flex-col @xl:flex-row items-start @xl:items-center justify-between gap-4 mb-6">
<div>
<div className="flex items-center gap-2">
<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 className="flex flex-col @xl:flex-row w-full @xl:w-auto gap-2">
{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>
<CreateVolumeForm
initialValues={{ ...volume, ...volume.config }}
onSubmit={handleSubmit}
mode="update"
loading={updateMutation.isPending}
/>
</Card>
<CreateVolumeForm
initialValues={{ ...volume, ...volume.config }}
onSubmit={() => {}}
mode="update"
readOnly
/>
</Card>
{hasLastError && (
<Card className="p-6">
<div className="space-y-2">
<p className="text-sm font-medium text-destructive">Last Error</p>
<p className="text-sm text-muted-foreground wrap-break-word">{volume.lastError}</p>
</div>
</Card>
)}
</div>
<div className="flex flex-col gap-4">
<div className="self-start w-full">
<HealthchecksCard volume={volume} />
@ -176,24 +158,6 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
</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}>
<AlertDialogContent>
<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 dashboardAdminIndexRouteImport } from './routes/(dashboard)/admin/index'
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 dashboardNotificationsCreateRouteImport } from './routes/(dashboard)/notifications/create'
import { Route as dashboardNotificationsNotificationIdRouteImport } from './routes/(dashboard)/notifications/$notificationId'
import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create'
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 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 dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit'
import { Route as dashboardBackupsBackupIdEditRouteImport } from './routes/(dashboard)/backups/$backupId/edit'
@ -108,12 +109,6 @@ const dashboardVolumesCreateRoute = dashboardVolumesCreateRouteImport.update({
path: '/volumes/create',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardVolumesVolumeIdRoute =
dashboardVolumesVolumeIdRouteImport.update({
id: '/volumes/$volumeId',
path: '/volumes/$volumeId',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesCreateRoute =
dashboardRepositoriesCreateRouteImport.update({
id: '/repositories/create',
@ -142,6 +137,12 @@ const authLoginErrorRoute = authLoginErrorRouteImport.update({
path: '/error',
getParentRoute: () => authLoginRoute,
} as any)
const dashboardVolumesVolumeIdIndexRoute =
dashboardVolumesVolumeIdIndexRouteImport.update({
id: '/volumes/$volumeId/',
path: '/volumes/$volumeId/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesRepositoryIdIndexRoute =
dashboardRepositoriesRepositoryIdIndexRouteImport.update({
id: '/repositories/$repositoryId/',
@ -154,6 +155,12 @@ const dashboardBackupsBackupIdIndexRoute =
path: '/backups/$backupId/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardVolumesVolumeIdEditRoute =
dashboardVolumesVolumeIdEditRouteImport.update({
id: '/volumes/$volumeId/edit',
path: '/volumes/$volumeId/edit',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardSettingsSsoNewRoute = dashboardSettingsSsoNewRouteImport.update({
id: '/settings/sso/new',
path: '/settings/sso/new',
@ -201,7 +208,6 @@ export interface FileRoutesByFullPath {
'/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
'/notifications/create': typeof dashboardNotificationsCreateRoute
'/repositories/create': typeof dashboardRepositoriesCreateRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/volumes/create': typeof dashboardVolumesCreateRoute
'/admin/': typeof dashboardAdminIndexRoute
'/backups/': typeof dashboardBackupsIndexRoute
@ -212,8 +218,10 @@ export interface FileRoutesByFullPath {
'/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
'/settings/sso/new': typeof dashboardSettingsSsoNewRoute
'/volumes/$volumeId/edit': typeof dashboardVolumesVolumeIdEditRoute
'/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
'/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
'/volumes/$volumeId/': typeof dashboardVolumesVolumeIdIndexRoute
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute
'/repositories/$repositoryId/$snapshotId/': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute
@ -229,7 +237,6 @@ export interface FileRoutesByTo {
'/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
'/notifications/create': typeof dashboardNotificationsCreateRoute
'/repositories/create': typeof dashboardRepositoriesCreateRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/volumes/create': typeof dashboardVolumesCreateRoute
'/admin': typeof dashboardAdminIndexRoute
'/backups': typeof dashboardBackupsIndexRoute
@ -240,8 +247,10 @@ export interface FileRoutesByTo {
'/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
'/settings/sso/new': typeof dashboardSettingsSsoNewRoute
'/volumes/$volumeId/edit': typeof dashboardVolumesVolumeIdEditRoute
'/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdIndexRoute
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute
'/repositories/$repositoryId/$snapshotId': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute
@ -260,7 +269,6 @@ export interface FileRoutesById {
'/(dashboard)/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
'/(dashboard)/notifications/create': typeof dashboardNotificationsCreateRoute
'/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
'/(dashboard)/volumes/create': typeof dashboardVolumesCreateRoute
'/(dashboard)/admin/': typeof dashboardAdminIndexRoute
'/(dashboard)/backups/': typeof dashboardBackupsIndexRoute
@ -271,8 +279,10 @@ export interface FileRoutesById {
'/(dashboard)/backups/$backupId/edit': typeof dashboardBackupsBackupIdEditRoute
'/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
'/(dashboard)/settings/sso/new': typeof dashboardSettingsSsoNewRoute
'/(dashboard)/volumes/$volumeId/edit': typeof dashboardVolumesVolumeIdEditRoute
'/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
'/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
'/(dashboard)/volumes/$volumeId/': typeof dashboardVolumesVolumeIdIndexRoute
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
'/(dashboard)/repositories/$repositoryId/$snapshotId/restore': typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute
'/(dashboard)/repositories/$repositoryId/$snapshotId/': typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute
@ -290,7 +300,6 @@ export interface FileRouteTypes {
| '/notifications/$notificationId'
| '/notifications/create'
| '/repositories/create'
| '/volumes/$volumeId'
| '/volumes/create'
| '/admin/'
| '/backups/'
@ -301,8 +310,10 @@ export interface FileRouteTypes {
| '/backups/$backupId/edit'
| '/repositories/$repositoryId/edit'
| '/settings/sso/new'
| '/volumes/$volumeId/edit'
| '/backups/$backupId/'
| '/repositories/$repositoryId/'
| '/volumes/$volumeId/'
| '/backups/$backupId/$snapshotId/restore'
| '/repositories/$repositoryId/$snapshotId/restore'
| '/repositories/$repositoryId/$snapshotId/'
@ -318,7 +329,6 @@ export interface FileRouteTypes {
| '/notifications/$notificationId'
| '/notifications/create'
| '/repositories/create'
| '/volumes/$volumeId'
| '/volumes/create'
| '/admin'
| '/backups'
@ -329,8 +339,10 @@ export interface FileRouteTypes {
| '/backups/$backupId/edit'
| '/repositories/$repositoryId/edit'
| '/settings/sso/new'
| '/volumes/$volumeId/edit'
| '/backups/$backupId'
| '/repositories/$repositoryId'
| '/volumes/$volumeId'
| '/backups/$backupId/$snapshotId/restore'
| '/repositories/$repositoryId/$snapshotId/restore'
| '/repositories/$repositoryId/$snapshotId'
@ -348,7 +360,6 @@ export interface FileRouteTypes {
| '/(dashboard)/notifications/$notificationId'
| '/(dashboard)/notifications/create'
| '/(dashboard)/repositories/create'
| '/(dashboard)/volumes/$volumeId'
| '/(dashboard)/volumes/create'
| '/(dashboard)/admin/'
| '/(dashboard)/backups/'
@ -359,8 +370,10 @@ export interface FileRouteTypes {
| '/(dashboard)/backups/$backupId/edit'
| '/(dashboard)/repositories/$repositoryId/edit'
| '/(dashboard)/settings/sso/new'
| '/(dashboard)/volumes/$volumeId/edit'
| '/(dashboard)/backups/$backupId/'
| '/(dashboard)/repositories/$repositoryId/'
| '/(dashboard)/volumes/$volumeId/'
| '/(dashboard)/backups/$backupId/$snapshotId/restore'
| '/(dashboard)/repositories/$repositoryId/$snapshotId/restore'
| '/(dashboard)/repositories/$repositoryId/$snapshotId/'
@ -473,13 +486,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardVolumesCreateRouteImport
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': {
id: '/(dashboard)/repositories/create'
path: '/repositories/create'
@ -515,6 +521,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authLoginErrorRouteImport
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/': {
id: '/(dashboard)/repositories/$repositoryId/'
path: '/repositories/$repositoryId'
@ -529,6 +542,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport
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': {
id: '/(dashboard)/settings/sso/new'
path: '/settings/sso/new'
@ -607,7 +627,6 @@ interface dashboardRouteRouteChildren {
dashboardNotificationsNotificationIdRoute: typeof dashboardNotificationsNotificationIdRoute
dashboardNotificationsCreateRoute: typeof dashboardNotificationsCreateRoute
dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
dashboardVolumesCreateRoute: typeof dashboardVolumesCreateRoute
dashboardAdminIndexRoute: typeof dashboardAdminIndexRoute
dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute
@ -618,8 +637,10 @@ interface dashboardRouteRouteChildren {
dashboardBackupsBackupIdEditRoute: typeof dashboardBackupsBackupIdEditRoute
dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute
dashboardSettingsSsoNewRoute: typeof dashboardSettingsSsoNewRoute
dashboardVolumesVolumeIdEditRoute: typeof dashboardVolumesVolumeIdEditRoute
dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute
dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute
dashboardVolumesVolumeIdIndexRoute: typeof dashboardVolumesVolumeIdIndexRoute
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdRestoreRoute
dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute: typeof dashboardRepositoriesRepositoryIdSnapshotIdIndexRoute
@ -631,7 +652,6 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardNotificationsNotificationIdRoute,
dashboardNotificationsCreateRoute: dashboardNotificationsCreateRoute,
dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute,
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
dashboardVolumesCreateRoute: dashboardVolumesCreateRoute,
dashboardAdminIndexRoute: dashboardAdminIndexRoute,
dashboardBackupsIndexRoute: dashboardBackupsIndexRoute,
@ -643,9 +663,11 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardRepositoriesRepositoryIdEditRoute:
dashboardRepositoriesRepositoryIdEditRoute,
dashboardSettingsSsoNewRoute: dashboardSettingsSsoNewRoute,
dashboardVolumesVolumeIdEditRoute: dashboardVolumesVolumeIdEditRoute,
dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute,
dashboardRepositoriesRepositoryIdIndexRoute:
dashboardRepositoriesRepositoryIdIndexRoute,
dashboardVolumesVolumeIdIndexRoute: dashboardVolumesVolumeIdIndexRoute,
dashboardBackupsBackupIdSnapshotIdRestoreRoute:
dashboardBackupsBackupIdSnapshotIdRestoreRoute,
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 { VolumeDetails } from "~/client/modules/volumes/routes/volume-details";
export const Route = createFileRoute("/(dashboard)/volumes/$volumeId")({
export const Route = createFileRoute("/(dashboard)/volumes/$volumeId/")({
component: RouteComponent,
errorComponent: (e) => <div>{e.error.message}</div>,
loader: async ({ params, context }) => {