Enhance backup validation and improve backup display

- Added validation to require a service name for compose backups in the backup handling logic, improving user feedback.
- Refactored the ShowBackups component to sort deployments by creation date and enhance the display of backup information, including service names and statuses, for better user experience.
- Updated logging in the compose backup utility to include command execution details, aiding in debugging and monitoring.
This commit is contained in:
Mauricio Siu
2025-05-03 12:59:22 -06:00
parent e437903ef8
commit 0690f07262
3 changed files with 196 additions and 167 deletions

View File

@@ -108,6 +108,15 @@ const Schema = z
path: ["databaseType"], path: ["databaseType"],
}); });
} }
if (data.backupType === "compose" && !data.serviceName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Service name is required for compose backups",
path: ["serviceName"],
});
}
if (data.backupType === "compose" && data.databaseType) { if (data.backupType === "compose" && data.databaseType) {
if (data.databaseType === "postgres") { if (data.databaseType === "postgres") {
if (!data.metadata?.postgres?.databaseUser) { if (!data.metadata?.postgres?.databaseUser) {

View File

@@ -178,184 +178,202 @@ export const ShowBackups = ({
</AlertBlock> </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}> const orderedDeployments = backup.deployments.sort(
<div className="flex w-full flex-col md:flex-row md:items-start justify-between gap-4 border rounded-lg p-4 hover:bg-muted/50 transition-colors"> (a, b) =>
<div className="flex flex-col w-full gap-4"> new Date(b.createdAt).getTime() -
<div className="flex items-center gap-3"> new Date(a.createdAt).getTime(),
{backup.backupType === "compose" && ( );
<div className="flex items-center justify-center size-10 rounded-lg">
{backup.databaseType === "postgres" && ( const serverId =
<PostgresqlIcon className="size-7" /> "serverId" in postgres ? postgres.serverId : undefined;
)}
{backup.databaseType === "mysql" && ( return (
<MysqlIcon className="size-7" /> <div key={backup.backupId}>
)} <div className="flex w-full flex-col md:flex-row md:items-start justify-between gap-4 border rounded-lg p-4 hover:bg-muted/50 transition-colors">
{backup.databaseType === "mariadb" && ( <div className="flex flex-col w-full gap-4">
<MariadbIcon className="size-7" /> <div className="flex items-center gap-3">
)}
{backup.databaseType === "mongo" && (
<MongodbIcon className="size-7" />
)}
</div>
)}
<div className="flex flex-col gap-1">
{backup.backupType === "compose" && ( {backup.backupType === "compose" && (
<div className="flex items-center gap-2"> <div className="flex items-center justify-center size-10 rounded-lg">
<h3 className="font-medium"> {backup.databaseType === "postgres" && (
{backup.serviceName} <PostgresqlIcon className="size-7" />
</h3> )}
<span className="px-1.5 py-0.5 rounded-full bg-muted text-xs font-medium capitalize"> {backup.databaseType === "mysql" && (
{backup.databaseType} <MysqlIcon className="size-7" />
</span> )}
{backup.databaseType === "mariadb" && (
<MariadbIcon className="size-7" />
)}
{backup.databaseType === "mongo" && (
<MongodbIcon className="size-7" />
)}
</div> </div>
)} )}
<div className="flex items-center gap-2"> <div className="flex flex-col gap-1">
<div {backup.backupType === "compose" && (
className={cn( <div className="flex items-center gap-2">
"size-1.5 rounded-full", <h3 className="font-medium">
backup.enabled {backup.serviceName}
? "bg-green-500" </h3>
: "bg-red-500", <span className="px-1.5 py-0.5 rounded-full bg-muted text-xs font-medium capitalize">
)} {backup.databaseType}
/> </span>
<span className="text-xs text-muted-foreground"> </div>
{backup.enabled ? "Active" : "Inactive"} )}
<div className="flex items-center gap-2">
<div
className={cn(
"size-1.5 rounded-full",
backup.enabled
? "bg-green-500"
: "bg-red-500",
)}
/>
<span className="text-xs text-muted-foreground">
{backup.enabled ? "Active" : "Inactive"}
</span>
</div>
</div>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<div className="min-w-[200px]">
<span className="text-sm font-medium text-muted-foreground">
Destination
</span> </span>
<p className="font-medium text-sm mt-0.5">
{backup.destination.name}
</p>
</div>
<div className="min-w-[150px]">
<span className="text-sm font-medium text-muted-foreground">
Database
</span>
<p className="font-medium text-sm mt-0.5">
{backup.database}
</p>
</div>
<div className="min-w-[120px]">
<span className="text-sm font-medium text-muted-foreground">
Schedule
</span>
<p className="font-medium text-sm mt-0.5">
{backup.schedule}
</p>
</div>
<div className="min-w-[150px]">
<span className="text-sm font-medium text-muted-foreground">
Prefix Storage
</span>
<p className="font-medium text-sm mt-0.5">
{backup.prefix}
</p>
</div>
<div className="min-w-[100px]">
<span className="text-sm font-medium text-muted-foreground">
Keep Latest
</span>
<p className="font-medium text-sm mt-0.5">
{backup.keepLatestCount || "All"}
</p>
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-x-8 gap-y-2"> <div className="flex flex-row md:flex-col gap-1.5">
<div className="min-w-[200px]"> <ShowSchedulesLogs
<span className="text-sm font-medium text-muted-foreground"> deployments={orderedDeployments}
Destination serverId={serverId || undefined}
</span> >
<p className="font-medium text-sm mt-0.5"> <Button
{backup.destination.name} variant="ghost"
</p> size="icon"
</div> className="size-8"
>
<ClipboardList className="size-4 transition-colors " />
</Button>
</ShowSchedulesLogs>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="size-8"
isLoading={
isManualBackup &&
activeManualBackup === backup.backupId
}
onClick={async () => {
setActiveManualBackup(backup.backupId);
await manualBackup({
backupId: backup.backupId as string,
})
.then(async () => {
toast.success(
"Manual Backup Successful",
);
})
.catch(() => {
toast.error(
"Error creating the manual backup",
);
});
setActiveManualBackup(undefined);
}}
>
<Play className="size-4 " />
</Button>
</TooltipTrigger>
<TooltipContent>
Run Manual Backup
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="min-w-[150px]"> <HandleBackup
<span className="text-sm font-medium text-muted-foreground"> backupType={backup.backupType}
Database backupId={backup.backupId}
</span> refetch={refetch}
<p className="font-medium text-sm mt-0.5"> />
{backup.database} <DialogAction
</p> title="Delete Backup"
</div> description="Are you sure you want to delete this backup?"
type="destructive"
<div className="min-w-[120px]"> onClick={async () => {
<span className="text-sm font-medium text-muted-foreground"> await deleteBackup({
Schedule backupId: backup.backupId,
</span> })
<p className="font-medium text-sm mt-0.5"> .then(() => {
{backup.schedule} refetch();
</p> toast.success(
</div> "Backup deleted successfully",
);
<div className="min-w-[150px]"> })
<span className="text-sm font-medium text-muted-foreground"> .catch(() => {
Prefix Storage toast.error("Error deleting backup");
</span> });
<p className="font-medium text-sm mt-0.5"> }}
{backup.prefix} >
</p> <Button
</div> variant="ghost"
size="icon"
<div className="min-w-[100px]"> className="group hover:bg-red-500/10 size-8"
<span className="text-sm font-medium text-muted-foreground"> isLoading={isRemoving}
Keep Latest >
</span> <Trash2 className="size-4 text-primary group-hover:text-red-500" />
<p className="font-medium text-sm mt-0.5"> </Button>
{backup.keepLatestCount || "All"} </DialogAction>
</p>
</div>
</div> </div>
</div> </div>
<div className="flex flex-row md:flex-col gap-1.5">
<ShowSchedulesLogs deployments={backup.deployments}>
<Button
variant="ghost"
size="icon"
className="size-8"
>
<ClipboardList className="size-4 transition-colors " />
</Button>
</ShowSchedulesLogs>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="size-8"
isLoading={
isManualBackup &&
activeManualBackup === backup.backupId
}
onClick={async () => {
setActiveManualBackup(backup.backupId);
await manualBackup({
backupId: backup.backupId as string,
})
.then(async () => {
toast.success(
"Manual Backup Successful",
);
})
.catch(() => {
toast.error(
"Error creating the manual backup",
);
});
setActiveManualBackup(undefined);
}}
>
<Play className="size-4 " />
</Button>
</TooltipTrigger>
<TooltipContent>Run Manual Backup</TooltipContent>
</Tooltip>
</TooltipProvider>
<HandleBackup
backupType={backup.backupType}
backupId={backup.backupId}
refetch={refetch}
/>
<DialogAction
title="Delete Backup"
description="Are you sure you want to delete this backup?"
type="destructive"
onClick={async () => {
await deleteBackup({
backupId: backup.backupId,
})
.then(() => {
refetch();
toast.success("Backup deleted successfully");
})
.catch(() => {
toast.error("Error deleting backup");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 size-8"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div> </div>
</div> );
))} })}
</div> </div>
</div> </div>
)} )}

View File

@@ -33,6 +33,7 @@ export const runComposeBackup = async (
title: "Compose Backup", title: "Compose Backup",
description: "Compose Backup", description: "Compose Backup",
}); });
try { try {
const rcloneFlags = getS3Credentials(destination); const rcloneFlags = getS3Credentials(destination);
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
@@ -77,7 +78,8 @@ export const runComposeBackup = async (
compose.serverId, compose.serverId,
` `
set -e; set -e;
Running command. echo "Running command." >> ${deployment.logPath};
export RCLONE_LOG_LEVEL=DEBUG;
${backupCommand} | ${rcloneCommand} >> ${deployment.logPath} 2>> ${deployment.logPath} || { ${backupCommand} | ${rcloneCommand} >> ${deployment.logPath} 2>> ${deployment.logPath} || {
echo "❌ Command failed" >> ${deployment.logPath}; echo "❌ Command failed" >> ${deployment.logPath};
exit 1; exit 1;