Refactor RestoreBackup component to enhance validation schema and improve database type handling. Update metadata requirements for different database types and streamline form initialization. Add alert for compose backups in ShowBackups to inform users about running services.

This commit is contained in:
Mauricio Siu 2025-05-01 19:48:47 -06:00
parent 4afc6ac250
commit c8e2f4bfdc
2 changed files with 137 additions and 82 deletions

View File

@ -65,74 +65,130 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
type DatabaseType =
| Exclude<ServiceType, "application" | "redis">
| "web-server";
interface Props { interface Props {
id: string; id: string;
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server"; databaseType: DatabaseType;
serverId?: string | null; serverId?: string | null;
backupType?: "database" | "compose"; backupType?: "database" | "compose";
} }
const getMetadataSchema = ( const RestoreBackupSchema = z
backupType: "database" | "compose", .object({
databaseType: string, destinationId: z
) => { .string({
if (backupType !== "compose") return z.object({}).optional(); required_error: "Please select a destination",
})
const schemas = { .min(1, {
postgres: z.object({ message: "Destination is required",
databaseUser: z.string().min(1, "Database user is required"), }),
}), backupFile: z
mariadb: z.object({ .string({
databaseUser: z.string().min(1, "Database user is required"), required_error: "Please select a backup file",
databasePassword: z.string().min(1, "Database password is required"), })
}), .min(1, {
mongo: z.object({ message: "Backup file is required",
databaseUser: z.string().min(1, "Database user is required"), }),
databasePassword: z.string().min(1, "Database password is required"), databaseName: z
}), .string({
mysql: z.object({ required_error: "Please enter a database name",
databaseRootPassword: z.string().min(1, "Root password is required"), })
}), .min(1, {
"web-server": z.object({}), message: "Database name is required",
}; }),
databaseType: z
return z.object({ .enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
[databaseType]: schemas[databaseType as keyof typeof schemas], .optional(),
serviceName: z.string().min(1, "Service name is required"), backupType: z.enum(["database", "compose"]).default("database"),
serviceName: z.string().nullable().optional(),
metadata: z
.object({
postgres: z
.object({
databaseUser: z.string(),
})
.optional(),
mariadb: z
.object({
databaseUser: z.string(),
databasePassword: z.string(),
})
.optional(),
mongo: z
.object({
databaseUser: z.string(),
databasePassword: z.string(),
})
.optional(),
mysql: z
.object({
databaseRootPassword: z.string(),
})
.optional(),
})
.optional(),
})
.superRefine((data, ctx) => {
if (data.backupType === "compose" && !data.databaseType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Database type is required for compose backups",
path: ["databaseType"],
});
}
if (data.backupType === "compose" && data.databaseType) {
if (data.databaseType === "postgres") {
if (!data.metadata?.postgres?.databaseUser) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Database user is required for PostgreSQL",
path: ["metadata", "postgres", "databaseUser"],
});
}
} else if (data.databaseType === "mariadb") {
if (!data.metadata?.mariadb?.databaseUser) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Database user is required for MariaDB",
path: ["metadata", "mariadb", "databaseUser"],
});
}
if (!data.metadata?.mariadb?.databasePassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Database password is required for MariaDB",
path: ["metadata", "mariadb", "databasePassword"],
});
}
} else if (data.databaseType === "mongo") {
if (!data.metadata?.mongo?.databaseUser) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Database user is required for MongoDB",
path: ["metadata", "mongo", "databaseUser"],
});
}
if (!data.metadata?.mongo?.databasePassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Database password is required for MongoDB",
path: ["metadata", "mongo", "databasePassword"],
});
}
} else if (data.databaseType === "mysql") {
if (!data.metadata?.mysql?.databaseRootPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Root password is required for MySQL",
path: ["metadata", "mysql", "databaseRootPassword"],
});
}
}
}
}); });
};
const RestoreBackupSchema = z.object({
destinationId: z
.string({
required_error: "Please select a destination",
})
.min(1, {
message: "Destination is required",
}),
backupFile: z
.string({
required_error: "Please select a backup file",
})
.min(1, {
message: "Backup file is required",
}),
databaseName: z
.string({
required_error: "Please enter a database name",
})
.min(1, {
message: "Database name is required",
}),
databaseType: z
.string({
required_error: "Please select a database type",
})
.min(1, {
message: "Database type is required",
}),
metadata: z.object({}).optional(),
});
const formatBytes = (bytes: number): string => { const formatBytes = (bytes: number): string => {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return "0 Bytes";
@ -151,31 +207,24 @@ export const RestoreBackup = ({
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [selectedDatabaseType, setSelectedDatabaseType] = useState<string>(
backupType === "compose" ? "" : databaseType,
);
const { data: destinations = [] } = api.destination.all.useQuery(); const { data: destinations = [] } = api.destination.all.useQuery();
const schema = RestoreBackupSchema.extend({ const form = useForm<z.infer<typeof RestoreBackupSchema>>({
metadata: getMetadataSchema(backupType, selectedDatabaseType),
});
const form = useForm<z.infer<typeof schema>>({
defaultValues: { defaultValues: {
destinationId: "", destinationId: "",
backupFile: "", backupFile: "",
databaseName: databaseType === "web-server" ? "dokploy" : "", databaseName: databaseType === "web-server" ? "dokploy" : "",
databaseType: backupType === "compose" ? "" : databaseType, databaseType:
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
metadata: {}, metadata: {},
}, },
resolver: zodResolver(schema), resolver: zodResolver(RestoreBackupSchema),
}); });
const destionationId = form.watch("destinationId"); const destionationId = form.watch("destinationId");
const currentDatabaseType = form.watch("databaseType");
const metadata = form.watch("metadata"); const metadata = form.watch("metadata");
// console.log({ metadata });
const debouncedSetSearch = debounce((value: string) => { const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value); setDebouncedSearchTerm(value);
@ -204,7 +253,7 @@ export const RestoreBackup = ({
api.backup.restoreBackupWithLogs.useSubscription( api.backup.restoreBackupWithLogs.useSubscription(
{ {
databaseId: id, databaseId: id,
databaseType: form.watch("databaseType"), databaseType: currentDatabaseType as DatabaseType,
databaseName: form.watch("databaseName"), databaseName: form.watch("databaseName"),
backupFile: form.watch("backupFile"), backupFile: form.watch("backupFile"),
destinationId: form.watch("destinationId"), destinationId: form.watch("destinationId"),
@ -231,7 +280,7 @@ export const RestoreBackup = ({
}, },
); );
const onSubmit = async (data: z.infer<typeof schema>) => { const onSubmit = async (data: z.infer<typeof RestoreBackupSchema>) => {
if (backupType === "compose" && !data.databaseType) { if (backupType === "compose" && !data.databaseType) {
toast.error("Please select a database type"); toast.error("Please select a database type");
return; return;
@ -488,9 +537,9 @@ export const RestoreBackup = ({
<FormLabel>Database Type</FormLabel> <FormLabel>Database Type</FormLabel>
<Select <Select
value={field.value} value={field.value}
onValueChange={(value) => { onValueChange={(value: DatabaseType) => {
field.onChange(value); field.onChange(value);
setSelectedDatabaseType(value); form.setValue("metadata", {});
}} }}
> >
<SelectTrigger> <SelectTrigger>
@ -510,7 +559,7 @@ export const RestoreBackup = ({
<FormField <FormField
control={form.control} control={form.control}
name="metadata.serviceName" name="serviceName"
render={({ field }) => ( render={({ field }) => (
<FormItem className="w-full"> <FormItem className="w-full">
<FormLabel>Service Name</FormLabel> <FormLabel>Service Name</FormLabel>
@ -609,7 +658,7 @@ export const RestoreBackup = ({
)} )}
/> />
{selectedDatabaseType === "postgres" && ( {currentDatabaseType === "postgres" && (
<FormField <FormField
control={form.control} control={form.control}
name="metadata.postgres.databaseUser" name="metadata.postgres.databaseUser"
@ -625,7 +674,7 @@ export const RestoreBackup = ({
/> />
)} )}
{selectedDatabaseType === "mariadb" && ( {currentDatabaseType === "mariadb" && (
<> <>
<FormField <FormField
control={form.control} control={form.control}
@ -663,7 +712,7 @@ export const RestoreBackup = ({
</> </>
)} )}
{selectedDatabaseType === "mongo" && ( {currentDatabaseType === "mongo" && (
<> <>
<FormField <FormField
control={form.control} control={form.control}
@ -701,7 +750,7 @@ export const RestoreBackup = ({
</> </>
)} )}
{selectedDatabaseType === "mysql" && ( {currentDatabaseType === "mysql" && (
<FormField <FormField
control={form.control} control={form.control}
name="metadata.mysql.databaseRootPassword" name="metadata.mysql.databaseRootPassword"

View File

@ -28,6 +28,7 @@ import {
MysqlIcon, MysqlIcon,
PostgresqlIcon, PostgresqlIcon,
} from "@/components/icons/data-tools-icons"; } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
interface Props { interface Props {
id: string; id: string;
@ -163,6 +164,11 @@ export const ShowBackups = ({
</div> </div>
) : ( ) : (
<div className="flex flex-col pt-2 gap-4"> <div className="flex flex-col pt-2 gap-4">
{backupType === "compose" && (
<AlertBlock title="Compose Backups">
Make sure the compose is running before creating a backup.
</AlertBlock>
)}
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{postgres?.backups.map((backup) => ( {postgres?.backups.map((backup) => (
<div key={backup.backupId}> <div key={backup.backupId}>