refactor: update volume backup schema and form handling

- Modified the volume backup schema to enforce non-null constraints on volumeName and added serviceName.
- Removed unnecessary fields (type, hostPath) from the schema and updated related API and form handling.
- Enhanced form validation to ensure required fields are properly checked.
- Updated the UI components to reflect changes in the volume backup management interface.
This commit is contained in:
Mauricio Siu
2025-06-29 23:05:36 -06:00
parent ce88a0a5f2
commit a5bba9a11b
8 changed files with 12279 additions and 27 deletions

View File

@@ -53,16 +53,12 @@ const formSchema = z
.object({
name: z.string().min(1, "Name is required"),
cronExpression: z.string().min(1, "Cron expression is required"),
volumeName: z.string(),
hostPath: z.string(),
volumeName: z.string().min(1, "Volume name is required"),
prefix: z.string(),
keepLatestCount: z.coerce.number().optional(),
turnOff: z.boolean().default(false),
volumeBackupType: z.enum(["bind", "volume"]),
enabled: z.boolean().default(true),
serviceName: z.string(),
destinationId: z.string().min(1, "Destination required"),
scheduleType: z.enum([
serviceType: z.enum([
"application",
"compose",
"postgres",
@@ -71,9 +67,11 @@ const formSchema = z
"mysql",
"redis",
]),
serviceName: z.string(),
destinationId: z.string().min(1, "Destination required"),
})
.superRefine((data, ctx) => {
if (data.scheduleType === "compose" && !data.serviceName) {
if (data.serviceType === "compose" && !data.serviceName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Service name is required",
@@ -110,18 +108,17 @@ export const HandleVolumeBackups = ({
name: "",
cronExpression: "",
volumeName: "",
hostPath: "",
prefix: "",
keepLatestCount: undefined,
turnOff: false,
volumeBackupType: "volume",
enabled: true,
serviceName: "",
serviceType: volumeBackupType,
},
});
const scheduleTypeForm = form.watch("scheduleType");
const serviceTypeForm = volumeBackupType;
const { data: destinations } = api.destination.all.useQuery();
const { data: volumeBackup } = api.volumeBackups.one.useQuery(
{ volumeBackupId: volumeBackupId || "" },
{ enabled: !!volumeBackupId },
@@ -150,14 +147,13 @@ export const HandleVolumeBackups = ({
name: volumeBackup.name,
cronExpression: volumeBackup.cronExpression,
volumeName: volumeBackup.volumeName || "",
hostPath: volumeBackup.hostPath || "",
prefix: volumeBackup.prefix,
keepLatestCount: volumeBackup.keepLatestCount,
keepLatestCount: volumeBackup.keepLatestCount || undefined,
turnOff: volumeBackup.turnOff,
volumeBackupType: volumeBackup.type,
enabled: volumeBackup.enabled,
enabled: volumeBackup.enabled || true,
serviceName: volumeBackup.serviceName || "",
destinationId: volumeBackup.destinationId,
serviceType: volumeBackup.serviceType,
});
}
}, [form, volumeBackup, volumeBackupId]);
@@ -173,6 +169,7 @@ export const HandleVolumeBackups = ({
...values,
destinationId: values.destinationId,
volumeBackupId: volumeBackupId || "",
serviceType: volumeBackupType,
...(volumeBackupType === "application" && {
applicationId: id || "",
}),
@@ -251,7 +248,7 @@ export const HandleVolumeBackups = ({
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{scheduleTypeForm === "compose" && (
{serviceTypeForm === "compose" && (
<div className="flex flex-col w-full gap-4">
{errorServices && (
<AlertBlock
@@ -440,6 +437,119 @@ export const HandleVolumeBackups = ({
)}
/>
<FormField
control={form.control}
name="destinationId"
render={({ field }) => (
<FormItem>
<FormLabel>Destination</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a destination" />
</SelectTrigger>
</FormControl>
<SelectContent>
{destinations?.map((destination) => (
<SelectItem
key={destination.destinationId}
value={destination.destinationId}
>
{destination.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Choose the backup destination where files will be stored
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="volumeName"
render={({ field }) => (
<FormItem>
<FormLabel>Volume Name</FormLabel>
<FormControl>
<Input placeholder="my-volume-name" {...field} />
</FormControl>
<FormDescription>
The name of the Docker volume to backup
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="prefix"
render={({ field }) => (
<FormItem>
<FormLabel>Backup Prefix</FormLabel>
<FormControl>
<Input placeholder="backup-" {...field} />
</FormControl>
<FormDescription>
Prefix for backup files (optional)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="keepLatestCount"
render={({ field }) => (
<FormItem>
<FormLabel>Keep Latest Count</FormLabel>
<FormControl>
<Input
type="number"
placeholder="5"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value) || undefined)
}
/>
</FormControl>
<FormDescription>
Number of backup files to keep (optional)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="turnOff"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
Turn Off Container During Backup
</FormLabel>
<FormDescription className="text-amber-600 dark:text-amber-400">
The container will be temporarily stopped during backup to
prevent file corruption. This ensures data integrity but may
cause temporary service interruption.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="enabled"

View File

@@ -0,0 +1,4 @@
ALTER TABLE "volume_backup" ALTER COLUMN "volumeName" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "volume_backup" DROP COLUMN "type";--> statement-breakpoint
ALTER TABLE "volume_backup" DROP COLUMN "hostPath";--> statement-breakpoint
DROP TYPE "public"."volumeBackupType";

View File

@@ -0,0 +1 @@
ALTER TABLE "volume_backup" ADD COLUMN "serviceName" text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -729,6 +729,20 @@
"when": 1751256796247,
"tag": "0103_mean_jimmy_woo",
"breakpoints": true
},
{
"idx": 104,
"version": "7",
"when": 1751259486912,
"tag": "0104_robust_nighthawk",
"breakpoints": true
},
{
"idx": 105,
"version": "7",
"when": 1751259917258,
"tag": "0105_unknown_firelord",
"breakpoints": true
}
]
}

View File

@@ -70,7 +70,7 @@ export const volumeBackupsRouter = createTRPCRouter({
runManually: protectedProcedure
.input(z.object({ volumeBackupId: z.string().min(1) }))
.mutation(async ({ input }) => {
.mutation(async () => {
// return await runVolumeBackupManually(input.volumeBackupId);
}),
});

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -13,19 +13,16 @@ import { postgres } from "./postgres";
import { mariadb } from "./mariadb";
import { destinations } from "./destination";
export const volumeBackupType = pgEnum("volumeBackupType", ["bind", "volume"]);
export const volumeBackups = pgTable("volume_backup", {
volumeBackupId: text("volumeBackupId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
type: volumeBackupType("type").notNull().default("volume"),
volumeName: text("volumeName"),
hostPath: text("hostPath"),
volumeName: text("volumeName").notNull(),
prefix: text("prefix").notNull(),
serviceType: serviceType("serviceType").notNull().default("application"),
serviceName: text("serviceName"),
turnOff: boolean("turnOff").notNull().default(false),
cronExpression: text("cronExpression").notNull(),
keepLatestCount: integer("keepLatestCount"),
@@ -95,10 +92,8 @@ export const volumeBackupsRelations = relations(volumeBackups, ({ one }) => ({
}),
}));
export const createVolumeBackupSchema = createInsertSchema(
volumeBackups,
).extend({
volumeName: z.string().min(1),
export const createVolumeBackupSchema = createInsertSchema(volumeBackups).omit({
volumeBackupId: true,
});
export const updateVolumeBackupSchema = createVolumeBackupSchema.extend({