fix: update RestoreVolumeBackups and ShowVolumeBackups components for improved functionality

- Refactored the RestoreVolumeBackups component to ensure the type prop is required and added serverId handling for better integration.
- Corrected variable naming for destinationId in the form handling to prevent potential issues.
- Enhanced the ShowVolumeBackups component to pass serverId to the RestoreVolumeBackups component, ensuring consistent data flow.
- Improved user interface elements for backup file selection, ensuring better usability and clarity.
This commit is contained in:
Mauricio Siu
2025-07-01 01:12:20 -06:00
parent 4f021a3f79
commit c6d760a904
7 changed files with 452 additions and 90 deletions

View File

@@ -47,35 +47,33 @@ import { formatBytes } from "../../database/backups/restore-backup";
interface Props {
id: string;
type?: "application" | "compose" | "postgres" | "mariadb" | "mongo" | "mysql";
type: "application" | "compose";
serverId?: string;
}
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",
}),
volumeName: z
.string({
required_error: "Please enter a volume name",
})
.min(1, {
message: "Volume name is required",
}),
})
.superRefine((data, ctx) => {});
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",
}),
volumeName: z
.string({
required_error: "Please enter a volume name",
})
.min(1, {
message: "Volume name is required",
}),
});
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
@@ -93,8 +91,9 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
resolver: zodResolver(RestoreBackupSchema),
});
const destionationId = form.watch("destinationId");
const destinationId = form.watch("destinationId");
const volumeName = form.watch("volumeName");
const backupFile = form.watch("backupFile");
const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
@@ -107,12 +106,12 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
{
destinationId: destionationId,
destinationId: destinationId,
search: debouncedSearchTerm,
serverId: serverId ?? "",
},
{
enabled: isOpen && !!destionationId,
enabled: isOpen && !!destinationId,
},
);
@@ -122,9 +121,12 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
api.volumeBackups.restoreVolumeBackupWithLogs.useSubscription(
{
volumeBackupId: id,
destinationId: form.watch("destinationId"),
volumeName: volumeName,
id,
serviceType: type,
serverId,
destinationId,
volumeName,
backupFileName: backupFile,
},
{
enabled: isDeploying,
@@ -146,7 +148,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
},
);
const onSubmit = async (data: z.infer<typeof RestoreBackupSchema>) => {
const onSubmit = async () => {
setIsDeploying(true);
};
@@ -246,10 +248,10 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
name="backupFile"
render={({ field }) => (
<FormItem className="">
<FormLabel className="flex items-center justify-between">
<FormLabel className="flex items-center">
Search Backup Files
{field.value && (
<Badge variant="outline">
<Badge variant="outline" className="truncate w-52">
{field.value}
<Copy
className="ml-2 size-4 cursor-pointer"
@@ -273,7 +275,9 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{field.value || "Search and select a backup file"}
<span className="truncate text-left flex-1 w-52">
{field.value || "Search and select a backup file"}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>

View File

@@ -29,10 +29,15 @@ import { RestoreVolumeBackups } from "./restore-volume-backups";
interface Props {
id: string;
type?: "application" | "compose" | "postgres" | "mariadb" | "mongo" | "mysql";
type?: "application" | "compose";
serverId?: string;
}
export const ShowVolumeBackups = ({ id, type = "application" }: Props) => {
export const ShowVolumeBackups = ({
id,
type = "application",
serverId,
}: Props) => {
const {
data: volumeBackups,
isLoading: isLoadingVolumeBackups,
@@ -74,7 +79,7 @@ export const ShowVolumeBackups = ({ id, type = "application" }: Props) => {
<HandleVolumeBackups id={id} volumeBackupType={type} />
)}
<div className="flex items-center gap-2">
<RestoreVolumeBackups id={id} type={type} />
<RestoreVolumeBackups id={id} type={type} serverId={serverId} />
</div>
</div>
</div>
@@ -228,7 +233,7 @@ export const ShowVolumeBackups = ({ id, type = "application" }: Props) => {
</p>
<div className="flex items-center gap-2">
<HandleVolumeBackups id={id} volumeBackupType={type} />
<RestoreVolumeBackups id={id} type={type} />
<RestoreVolumeBackups id={id} type={type} serverId={serverId} />
</div>
</div>
)}

View File

@@ -415,7 +415,7 @@ export const RestoreBackup = ({
<FormLabel className="flex items-center justify-between">
Search Backup Files
{field.value && (
<Badge variant="outline">
<Badge variant="outline" className="truncate">
{field.value}
<Copy
className="ml-2 size-4 cursor-pointer"
@@ -439,7 +439,9 @@ export const RestoreBackup = ({
!field.value && "text-muted-foreground",
)}
>
{field.value || "Search and select a backup file"}
<span className="truncate text-left flex-1 w-52">
{field.value || "Search and select a backup file"}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>

View File

@@ -338,6 +338,7 @@ const Service = (
<ShowVolumeBackups
id={applicationId}
type="application"
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>

View File

@@ -262,7 +262,11 @@ const Service = (
</TabsContent>
<TabsContent value="volumeBackups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowVolumeBackups id={composeId} type="compose" />
<ShowVolumeBackups
id={composeId}
type="compose"
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
<TabsContent value="monitoring">

View File

@@ -1,11 +1,10 @@
import {
findVolumeBackupById,
IS_CLOUD,
updateVolumeBackup,
removeVolumeBackup,
createVolumeBackup,
runVolumeBackup,
findDestinationById,
findVolumeBackupById,
} from "@dokploy/server";
import {
createVolumeBackupSchema,
@@ -16,8 +15,12 @@ import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@dokploy/server/db";
import { eq } from "drizzle-orm";
import { restorePostgresBackup } from "@dokploy/server/utils/restore";
import { observable } from "@trpc/server/observable";
import { restoreVolume } from "@dokploy/server/utils/volume-backups/utils";
import {
execAsyncRemote,
execAsyncStream,
} from "@dokploy/server/utils/process/execAsync";
export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure
@@ -102,18 +105,76 @@ export const volumeBackupsRouter = createTRPCRouter({
})
.input(
z.object({
volumeBackupId: z.string().min(1),
backupFileName: z.string().min(1),
destinationId: z.string().min(1),
volumeName: z.string().min(1),
id: z.string().min(1),
serviceType: z.enum(["application", "compose"]),
serverId: z.string().optional(),
}),
)
.subscription(async ({ input }) => {
const destination = await findDestinationById(input.destinationId);
return observable<string>((emit) => {
// restorePostgresBackup(postgres, destination, input, (log) => {
// emit.next(log);
// });
const runRestore = async () => {
try {
emit.next("🚀 Starting volume restore process...");
emit.next(`📂 Backup File: ${input.backupFileName}`);
emit.next(`🔧 Volume Name: ${input.volumeName}`);
emit.next(`🏷️ Service Type: ${input.serviceType}`);
emit.next(""); // Empty line for better readability
// Generate the restore command
const restoreCommand = await restoreVolume(
input.id,
input.destinationId,
input.volumeName,
input.backupFileName,
input.serverId || "",
input.serviceType,
);
emit.next("📋 Generated restore command:");
emit.next("▶️ Executing restore...");
emit.next(""); // Empty line
// Execute the restore command with real-time output
if (input.serverId) {
emit.next(`🌐 Executing on remote server: ${input.serverId}`);
await execAsyncRemote(input.serverId, restoreCommand, (data) => {
emit.next(data);
});
} else {
emit.next("🖥️ Executing on local server");
await execAsyncStream(restoreCommand, (data) => {
emit.next(data);
});
}
emit.next("");
emit.next("✅ Volume restore completed successfully!");
emit.next(
"🎉 All containers/services have been restarted with the restored volume.",
);
} catch (error) {
emit.next("");
emit.next("❌ Volume restore failed!");
emit.next(
`💥 Error: ${error instanceof Error ? error.message : "Unknown error"}`,
);
if (error instanceof Error && error.stack) {
emit.next("📋 Stack trace:");
for (const line of error.stack.split("\n")) {
if (line.trim()) emit.next(` ${line}`);
}
}
} finally {
emit.complete();
}
};
// Start the restore process
runRestore();
});
}),
});