refactor: frontend components consolidation

This commit is contained in:
Nicolas Meienberger
2025-11-01 17:49:40 +01:00
parent 18115b374c
commit 3befa127d7
30 changed files with 483 additions and 449 deletions

View File

@@ -1,112 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { CalendarClock, Database, HardDrive, Plus } from "lucide-react";
import { Link } from "react-router";
import { listBackupSchedulesOptions } from "~/api-client/@tanstack/react-query.gen";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
export default function BackupJobsPage() {
const { data: schedules, isLoading } = useQuery({
...listBackupSchedulesOptions(),
refetchInterval: 10000,
refetchOnWindowFocus: true,
});
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Loading backup schedules...</p>
</div>
);
}
if (!schedules || schedules.length === 0) {
return (
<Card>
<CardContent className="py-16">
<div className="flex flex-col items-center justify-center text-center">
<div className="relative mb-6">
<div className="absolute inset-0 animate-pulse">
<div className="w-24 h-24 rounded-full bg-primary/10 blur-2xl" />
</div>
<div className="relative flex items-center justify-center w-24 h-24 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/20">
<CalendarClock className="w-16 h-16 text-primary/70" strokeWidth={1.5} />
</div>
</div>
<h3 className="text-xl font-semibold mb-2">No backup job created</h3>
<p className="text-muted-foreground text-sm mb-6 max-w-md">
Backup jobs allow you to create automated backup schedules for your volumes. Set up your first backup job
to ensure your data is securely backed up.
</p>
<Button>
<Link to="/repositories" className="flex items-center">
<Plus className="h-4 w-4 mr-2" />
Create a backup job
</Link>
</Button>
</div>
</CardContent>
</Card>
);
}
return (
<div className="container mx-auto space-y-6">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{schedules.map((schedule) => (
<Link key={schedule.id} to={`/backup-jobs/${schedule.id}`}>
<Card key={schedule.id} className="flex flex-col">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 flex-1 min-w-0">
<HardDrive className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<CardTitle className="text-lg truncate">Volume #{schedule.volumeId}</CardTitle>
</div>
<Badge variant={schedule.enabled ? "default" : "secondary"} className="flex-shrink-0">
{schedule.enabled ? "Active" : "Paused"}
</Badge>
</div>
<CardDescription className="flex items-center gap-2 mt-2">
<Database className="h-4 w-4" />
<span className="truncate">{schedule.repositoryId}</span>
</CardDescription>
</CardHeader>
<CardContent className="flex-1 space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Schedule</span>
<code className="text-xs bg-muted px-2 py-1 rounded">{schedule.cronExpression}</code>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Last backup</span>
<span className="font-medium">
{schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleDateString() : "Never"}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Next backup</span>
<span className="font-medium">
{schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleDateString() : "N/A"}
</span>
</div>
{schedule.lastBackupStatus && (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Status</span>
<Badge
variant={schedule.lastBackupStatus === "success" ? "default" : "destructive"}
className="text-xs"
>
{schedule.lastBackupStatus}
</Badge>
</div>
)}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
);
}

View File

@@ -1,158 +0,0 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useNavigate, useParams, useSearchParams } from "react-router";
import { toast } from "sonner";
import {
deleteVolumeMutation,
getVolumeOptions,
mountVolumeMutation,
unmountVolumeMutation,
} from "~/api-client/@tanstack/react-query.gen";
import { StatusDot } from "~/components/status-dot";
import { Button } from "~/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { VolumeIcon } from "~/components/volume-icon";
import { parseError } from "~/lib/errors";
import { cn } from "~/lib/utils";
import { VolumeBackupsTabContent } from "~/modules/details/tabs/backups";
import { DockerTabContent } from "~/modules/details/tabs/docker";
import { FilesTabContent } from "~/modules/details/tabs/files";
import { VolumeInfoTabContent } from "~/modules/details/tabs/info";
import { getVolume } from "../api-client";
import type { Route } from "./+types/details";
export function meta({ params }: Route.MetaArgs) {
return [
{ title: `Ironmount - ${params.name}` },
{
name: "description",
content: "Create, manage, monitor, and automate your Docker volumes with ease.",
},
];
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const volume = await getVolume({ path: { name: params.name ?? "" } });
if (volume.data) return volume.data;
};
export default function DetailsPage({ loaderData }: Route.ComponentProps) {
const { name } = useParams<{ name: string }>();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get("tab") || "info";
const { data } = useQuery({
...getVolumeOptions({ path: { name: name ?? "" } }),
initialData: loaderData,
refetchInterval: 10000,
refetchOnWindowFocus: true,
});
const deleteVol = useMutation({
...deleteVolumeMutation(),
onSuccess: () => {
toast.success("Volume deleted successfully");
navigate("/volumes");
},
onError: (error) => {
toast.error("Failed to delete volume", {
description: parseError(error)?.message,
});
},
});
const mountVol = useMutation({
...mountVolumeMutation(),
onSuccess: () => {
toast.success("Volume mounted successfully");
},
onError: (error) => {
toast.error("Failed to mount volume", {
description: parseError(error)?.message,
});
},
});
const unmountVol = useMutation({
...unmountVolumeMutation(),
onSuccess: () => {
toast.success("Volume unmounted successfully");
},
onError: (error) => {
toast.error("Failed to unmount volume", {
description: parseError(error)?.message,
});
},
});
const handleDeleteConfirm = (name: string) => {
if (confirm(`Are you sure you want to delete the volume "${name}"? This action cannot be undone.`)) {
deleteVol.mutate({ path: { name } });
}
};
if (!name) {
return <div>Volume not found</div>;
}
if (!data) {
return <div>Loading...</div>;
}
const { volume, statfs } = data;
return (
<>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-semibold mb-2 text-muted-foreground flex items-center gap-2">
<span className="flex items-center gap-2">
<StatusDot status={volume.status} /> {volume.status[0].toUpperCase() + volume.status.slice(1)}
</span>
<VolumeIcon size={14} backend={volume?.config.backend} />
</div>
</div>
<div className="flex gap-4">
<Button
onClick={() => mountVol.mutate({ path: { name } })}
loading={mountVol.isPending}
className={cn({ hidden: volume.status === "mounted" })}
>
Mount
</Button>
<Button
variant="secondary"
onClick={() => unmountVol.mutate({ path: { name } })}
loading={unmountVol.isPending}
className={cn({ hidden: volume.status !== "mounted" })}
>
Unmount
</Button>
<Button variant="destructive" onClick={() => handleDeleteConfirm(name)} disabled={deleteVol.isPending}>
Delete
</Button>
</div>
</div>
<Tabs value={activeTab} onValueChange={(value) => setSearchParams({ tab: value })} className="mt-4">
<TabsList className="mb-2">
<TabsTrigger value="info">Configuration</TabsTrigger>
<TabsTrigger value="files">Files</TabsTrigger>
<TabsTrigger value="docker">Docker</TabsTrigger>
<TabsTrigger value="backups">Backups</TabsTrigger>
</TabsList>
<TabsContent value="info">
<VolumeInfoTabContent volume={volume} statfs={statfs} />
</TabsContent>
<TabsContent value="files">
<FilesTabContent volume={volume} />
</TabsContent>
<TabsContent value="docker">
<DockerTabContent volume={volume} />
</TabsContent>
<TabsContent value="backups">
<VolumeBackupsTabContent volume={volume} />
</TabsContent>
</Tabs>
</>
);
}

View File

@@ -1,186 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { listVolumes } from "~/api-client";
import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
import { EmptyState } from "~/components/empty-state";
import { StatusDot } from "~/components/status-dot";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
import { VolumeIcon } from "~/components/volume-icon";
import { copyToClipboard } from "~/utils/clipboard";
import type { Route } from "./+types/home";
export function meta(_: Route.MetaArgs) {
return [
{ title: "Ironmount" },
{
name: "description",
content: "Create, manage, monitor, and automate your Docker volumes with ease.",
},
];
}
export const clientLoader = async () => {
const volumes = await listVolumes();
if (volumes.data) return { volumes: volumes.data.volumes };
return { volumes: [] };
};
export default function Home({ loaderData }: Route.ComponentProps) {
const [createVolumeOpen, setCreateVolumeOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState("");
const [backendFilter, setBackendFilter] = useState("");
const clearFilters = () => {
setSearchQuery("");
setStatusFilter("");
setBackendFilter("");
};
const navigate = useNavigate();
const { data } = useQuery({
...listVolumesOptions(),
initialData: loaderData,
refetchInterval: 10000,
refetchOnWindowFocus: true,
});
const filteredVolumes =
data?.volumes.filter((volume) => {
const matchesSearch = volume.name.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = !statusFilter || volume.status === statusFilter;
const matchesBackend = !backendFilter || volume.type === backendFilter;
return matchesSearch && matchesStatus && matchesBackend;
}) || [];
const hasNoVolumes = data?.volumes.length === 0;
const hasNoFilteredVolumes = filteredVolumes.length === 0 && !hasNoVolumes;
if (hasNoVolumes) {
return (
<Card className="p-0 gap-0">
<EmptyState />
</Card>
);
}
return (
<Card className="p-0 gap-0">
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-2 md:justify-between p-4 bg-card-header py-4">
<span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap ">
<Input
className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]"
placeholder="Search volumes…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]">
<SelectValue placeholder="All status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mounted">Mounted</SelectItem>
<SelectItem value="unmounted">Unmounted</SelectItem>
<SelectItem value="error">Error</SelectItem>
</SelectContent>
</Select>
<Select value={backendFilter} onValueChange={setBackendFilter}>
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mt-[-1px]">
<SelectValue placeholder="All backends" />
</SelectTrigger>
<SelectContent>
<SelectItem value="directory">Directory</SelectItem>
<SelectItem value="nfs">NFS</SelectItem>
<SelectItem value="smb">SMB</SelectItem>
</SelectContent>
</Select>
{(searchQuery || statusFilter || backendFilter) && (
<Button onClick={clearFilters} className="w-full lg:w-auto mt-2 lg:mt-0 lg:ml-2">
<RotateCcw className="h-4 w-4 mr-2" />
Clear filters
</Button>
)}
</span>
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
</div>
<div className="overflow-x-auto">
<Table className="border-t">
<TableHeader className="bg-card-header">
<TableRow>
<TableHead className="w-[100px] uppercase">Name</TableHead>
<TableHead className="uppercase text-left">Backend</TableHead>
<TableHead className="uppercase hidden sm:table-cell">Mountpoint</TableHead>
<TableHead className="uppercase text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{hasNoFilteredVolumes ? (
<TableRow>
<TableCell colSpan={4} className="text-center py-12">
<div className="flex flex-col items-center gap-3">
<p className="text-muted-foreground">No volumes match your filters.</p>
<Button onClick={clearFilters} variant="outline" size="sm">
<RotateCcw className="h-4 w-4 mr-2" />
Clear filters
</Button>
</div>
</TableCell>
</TableRow>
) : (
filteredVolumes.map((volume) => (
<TableRow
key={volume.name}
className="hover:bg-accent/50 hover:cursor-pointer"
onClick={() => navigate(`/volumes/${volume.name}`)}
>
<TableCell className="font-medium text-strong-accent">{volume.name}</TableCell>
<TableCell>
<VolumeIcon backend={volume.type} />
</TableCell>
<TableCell className="hidden sm:table-cell">
<button
type="button"
className="flex items-center gap-2 cursor-pointer hover:opacity-70 transition-opacity"
onClick={(e) => {
e.stopPropagation();
copyToClipboard(volume.path);
toast.success("Path copied to clipboard");
}}
>
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
{volume.path}
</span>
<Copy size={10} />
</button>
</TableCell>
<TableCell className="text-center">
<StatusDot status={volume.status} />
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-end border-t">
{hasNoFilteredVolumes ? (
"No volumes match filters."
) : (
<span>
<span className="text-strong-accent">{filteredVolumes.length}</span> volume
{filteredVolumes.length > 1 ? "s" : ""}
</span>
)}
</div>
</Card>
);
}

View File

@@ -1,100 +0,0 @@
import { arktypeResolver } from "@hookform/resolvers/arktype";
import { useMutation } from "@tanstack/react-query";
import { type } from "arktype";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { loginMutation } from "~/api-client/@tanstack/react-query.gen";
import { AuthLayout } from "~/components/auth-layout";
import { Button } from "~/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
import { Input } from "~/components/ui/input";
import { authMiddleware } from "~/middleware/auth";
export const clientMiddleware = [authMiddleware];
const loginSchema = type({
username: "2<=string<=50",
password: "string>=1",
});
type LoginFormValues = typeof loginSchema.inferIn;
export default function LoginPage() {
const navigate = useNavigate();
const form = useForm<LoginFormValues>({
resolver: arktypeResolver(loginSchema),
defaultValues: {
username: "",
password: "",
},
});
const login = useMutation({
...loginMutation(),
onSuccess: async () => {
navigate("/volumes");
},
onError: (error) => {
console.error(error);
toast.error("Login failed", { description: error.message });
},
});
const onSubmit = (values: LoginFormValues) => {
login.mutate({
body: {
username: values.username.trim(),
password: values.password.trim(),
},
});
};
return (
<AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} type="text" placeholder="admin" disabled={login.isPending} autoFocus />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Password</FormLabel>
<button
type="button"
className="text-xs text-muted-foreground hover:underline"
onClick={() => toast.info("Password reset not implemented")}
>
Forgot your password?
</button>
</div>
<FormControl>
<Input {...field} type="password" disabled={login.isPending} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" loading={login.isPending}>
Login
</Button>
</form>
</Form>
</AuthLayout>
);
}

View File

@@ -1,127 +0,0 @@
import { arktypeResolver } from "@hookform/resolvers/arktype";
import { useMutation } from "@tanstack/react-query";
import { type } from "arktype";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { registerMutation } from "~/api-client/@tanstack/react-query.gen";
import { AuthLayout } from "~/components/auth-layout";
import { Button } from "~/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
import { Input } from "~/components/ui/input";
import { authMiddleware } from "~/middleware/auth";
export const clientMiddleware = [authMiddleware];
const onboardingSchema = type({
username: "2<=string<=50",
password: "string>=8",
confirmPassword: "string>=1",
});
type OnboardingFormValues = typeof onboardingSchema.inferIn;
export default function OnboardingPage() {
const navigate = useNavigate();
const form = useForm<OnboardingFormValues>({
resolver: arktypeResolver(onboardingSchema),
defaultValues: {
username: "",
password: "",
confirmPassword: "",
},
});
const registerUser = useMutation({
...registerMutation(),
onSuccess: async () => {
toast.success("Admin user created successfully!");
navigate("/volumes");
},
onError: (error) => {
console.error(error);
toast.error("Failed to create admin user", { description: error.message });
},
});
const onSubmit = (values: OnboardingFormValues) => {
if (values.password !== values.confirmPassword) {
form.setError("confirmPassword", {
type: "manual",
message: "Passwords do not match",
});
return;
}
registerUser.mutate({
body: {
username: values.username.trim(),
password: values.password.trim(),
},
});
};
return (
<AuthLayout title="Welcome to Ironmount" description="Create the admin user to get started">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} type="text" placeholder="admin" disabled={registerUser.isPending} autoFocus />
</FormControl>
<FormDescription>Choose a username for the admin account</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
{...field}
type="password"
placeholder="Enter a secure password"
disabled={registerUser.isPending}
/>
</FormControl>
<FormDescription>Password must be at least 8 characters long.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
{...field}
type="password"
placeholder="Re-enter your password"
disabled={registerUser.isPending}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" loading={registerUser.isPending}>
Create Admin User
</Button>
</form>
</Form>
</AuthLayout>
);
}

View File

@@ -1,175 +0,0 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Link, useParams } from "react-router";
import { toast } from "sonner";
import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
import {
upsertBackupScheduleMutation,
getBackupScheduleOptions,
runBackupNowMutation,
} from "~/api-client/@tanstack/react-query.gen";
import { parseError } from "~/lib/errors";
import { CreateScheduleForm, type BackupScheduleFormValues } from "~/modules/details/components/create-schedule-form";
import { ScheduleSummary } from "~/modules/details/components/schedule-summary";
const getCronExpression = (frequency: string, dailyTime?: string, weeklyDay?: string): string => {
if (frequency === "hourly") {
return "0 * * * *";
}
if (!dailyTime) {
dailyTime = "02:00";
}
const [hours, minutes] = dailyTime.split(":");
if (frequency === "daily") {
return `${minutes} ${hours} * * *`;
}
return `${minutes} ${hours} * * ${weeklyDay ?? "0"}`;
};
export default function ScheduleDetailsPage() {
const { scheduleId } = useParams<{ scheduleId: string }>();
const queryClient = useQueryClient();
const [isEditMode, setIsEditMode] = useState(false);
const { data: schedule, isLoading: loadingSchedule } = useQuery({
...getBackupScheduleOptions({
path: { scheduleId: scheduleId || "" },
}),
});
console.log("Schedule Details:", schedule);
const upsertSchedule = useMutation({
...upsertBackupScheduleMutation(),
onSuccess: () => {
toast.success("Backup schedule saved successfully");
queryClient.invalidateQueries({ queryKey: ["listBackupSchedules"] });
queryClient.invalidateQueries({ queryKey: ["getBackupSchedule", scheduleId] });
setIsEditMode(false);
},
onError: (error) => {
toast.error("Failed to save backup schedule", {
description: parseError(error)?.message,
});
},
});
const runBackupNow = useMutation({
...runBackupNowMutation(),
onSuccess: () => {
toast.success("Backup started successfully");
queryClient.invalidateQueries({ queryKey: ["getBackupSchedule", scheduleId] });
},
onError: (error) => {
toast.error("Failed to start backup", {
description: parseError(error)?.message,
});
},
});
const handleSubmit = (formValues: BackupScheduleFormValues) => {
if (!schedule) return;
const cronExpression = getCronExpression(formValues.frequency, formValues.dailyTime, formValues.weeklyDay);
const retentionPolicy: Record<string, number> = {};
if (formValues.keepLast) retentionPolicy.keepLast = formValues.keepLast;
if (formValues.keepHourly) retentionPolicy.keepHourly = formValues.keepHourly;
if (formValues.keepDaily) retentionPolicy.keepDaily = formValues.keepDaily;
if (formValues.keepWeekly) retentionPolicy.keepWeekly = formValues.keepWeekly;
if (formValues.keepMonthly) retentionPolicy.keepMonthly = formValues.keepMonthly;
if (formValues.keepYearly) retentionPolicy.keepYearly = formValues.keepYearly;
upsertSchedule.mutate({
body: {
volumeId: schedule.volumeId,
repositoryId: formValues.repositoryId,
enabled: schedule.enabled,
cronExpression,
retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined,
},
});
};
const handleToggleEnabled = (enabled: boolean) => {
if (!schedule) return;
upsertSchedule.mutate({
body: {
volumeId: schedule.volumeId,
repositoryId: schedule.repositoryId,
enabled,
cronExpression: schedule.cronExpression,
retentionPolicy: schedule.retentionPolicy || undefined,
},
});
};
const handleRunBackupNow = () => {
if (!schedule) return;
runBackupNow.mutate({
path: {
scheduleId: schedule.id.toString(),
},
});
};
if (loadingSchedule && !schedule) {
return (
<div className="container mx-auto p-4 sm:p-8">
<Card>
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">Loading...</p>
</CardContent>
</Card>
</div>
);
}
if (!schedule) {
return (
<div className="container mx-auto p-4 sm:p-8">
<Card>
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">Schedule not found</p>
<Button asChild className="mt-4">
<Link to="/backup-jobs">Back to Backup Jobs</Link>
</Button>
</CardContent>
</Card>
</div>
);
}
if (!isEditMode) {
return (
<div className="container mx-auto p-4 sm:p-8">
<ScheduleSummary
handleToggleEnabled={handleToggleEnabled}
handleRunBackupNow={handleRunBackupNow}
repository={schedule.repository}
setIsEditMode={setIsEditMode}
schedule={schedule}
volume={schedule.volume}
/>
</div>
);
}
return (
<div className="container mx-auto p-4 sm:p-8 space-y-4">
<div className="flex justify-end">
<Button variant="outline" onClick={() => setIsEditMode(false)}>
Cancel
</Button>
</div>
<CreateScheduleForm volume={schedule.volume} initialValues={schedule} onSubmit={handleSubmit} />
</div>
);
}