mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
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:
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user