refactor: dedicated edit page for volumes (#737)
This commit is contained in:
parent
d6e80b71d7
commit
4bf1463406
11 changed files with 428 additions and 253 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
136
app/client/modules/volumes/routes/edit-volume.tsx
Normal file
136
app/client/modules/volumes/routes/edit-volume.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
37
app/routes/(dashboard)/volumes/$volumeId/edit.tsx
Normal file
37
app/routes/(dashboard)/volumes/$volumeId/edit.tsx
Normal 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} />;
|
||||
}
|
||||
|
|
@ -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 }) => {
|
||||
Loading…
Reference in a new issue