Update deployment and backup components for improved handling and user experience

- Refined the `ShowDeployments` component to conditionally render `CancelQueues` and `RefreshToken` based on deployment type, enhancing flexibility.
- Removed unnecessary console logging in `RestoreBackup` and updated input field to disable when the database type is "web-server", improving user guidance.
- Enhanced logging in `runWebServerBackup` to provide clearer output during backup operations, ensuring better traceability of actions.
This commit is contained in:
Mauricio Siu
2025-05-04 13:19:45 -06:00
parent 06c9e43143
commit 9aa56870b0
6 changed files with 64 additions and 143 deletions

View File

@@ -8,7 +8,13 @@ import { ShowDeployments } from "./show-deployments";
interface Props { interface Props {
id: string; id: string;
type: "application" | "compose" | "schedule" | "server" | "backup"; type:
| "application"
| "compose"
| "schedule"
| "server"
| "backup"
| "previewDeployment";
serverId?: string; serverId?: string;
refreshToken?: string; refreshToken?: string;
children?: React.ReactNode; children?: React.ReactNode;

View File

@@ -18,7 +18,13 @@ import { Badge } from "@/components/ui/badge";
interface Props { interface Props {
id: string; id: string;
type: "application" | "compose" | "schedule" | "server" | "backup"; type:
| "application"
| "compose"
| "schedule"
| "server"
| "backup"
| "previewDeployment";
refreshToken?: string; refreshToken?: string;
serverId?: string; serverId?: string;
} }
@@ -65,7 +71,9 @@ export const ShowDeployments = ({
See all the 10 last deployments for this {type} See all the 10 last deployments for this {type}
</CardDescription> </CardDescription>
</div> </div>
{refreshToken && <CancelQueues id={id} type={type} />} {(type === "application" || type === "compose") && (
<CancelQueues id={id} type={type} />
)}
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
{refreshToken && ( {refreshToken && (
@@ -80,7 +88,9 @@ export const ShowDeployments = ({
<span className="break-all text-muted-foreground"> <span className="break-all text-muted-foreground">
{`${url}/api/deploy/${refreshToken}`} {`${url}/api/deploy/${refreshToken}`}
</span> </span>
<RefreshToken id={id} type={type} /> {(type === "application" || type === "compose") && (
<RefreshToken id={id} type={type} />
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,100 +0,0 @@
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import type { RouterOutputs } from "@/utils/api";
import { useState } from "react";
import { ShowDeployment } from "../deployments/show-deployment";
interface Props {
deployments: RouterOutputs["deployment"]["all"];
serverId?: string;
trigger?: React.ReactNode;
}
export const ShowPreviewBuilds = ({
deployments,
serverId,
trigger,
}: Props) => {
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{trigger ? (
trigger
) : (
<Button className="sm:w-auto w-full" size="sm" variant="outline">
View Builds
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
<DialogHeader>
<DialogTitle>Preview Builds</DialogTitle>
<DialogDescription>
See all the preview builds for this application on this Pull Request
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
{deployments?.map((deployment) => (
<div
key={deployment.deploymentId}
className="flex items-center justify-between rounded-lg border p-4 gap-2"
>
<div className="flex flex-col">
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{deployment.status}
<StatusTooltip
status={deployment?.status}
className="size-2.5"
/>
</span>
<span className="text-sm text-muted-foreground">
{deployment.title}
</span>
{deployment.description && (
<span className="break-all text-sm text-muted-foreground">
{deployment.description}
</span>
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground">
<DateTooltip date={deployment.createdAt} />
</div>
<Button
onClick={() => {
setActiveLog(deployment);
}}
>
View
</Button>
</div>
</div>
))}
</div>
</DialogContent>
<ShowDeployment
serverId={serverId || ""}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</Dialog>
);
};

View File

@@ -17,7 +17,7 @@ import {
ExternalLink, ExternalLink,
FileText, FileText,
GitPullRequest, GitPullRequest,
Layers, Loader2,
PenSquare, PenSquare,
RocketIcon, RocketIcon,
Trash2, Trash2,
@@ -25,8 +25,8 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { AddPreviewDomain } from "./add-preview-domain"; import { AddPreviewDomain } from "./add-preview-domain";
import { ShowPreviewBuilds } from "./show-preview-builds";
import { ShowPreviewSettings } from "./show-preview-settings"; import { ShowPreviewSettings } from "./show-preview-settings";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
interface Props { interface Props {
applicationId: string; applicationId: string;
@@ -38,13 +38,16 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { mutateAsync: deletePreviewDeployment, isLoading } = const { mutateAsync: deletePreviewDeployment, isLoading } =
api.previewDeployment.delete.useMutation(); api.previewDeployment.delete.useMutation();
const { data: previewDeployments, refetch: refetchPreviewDeployments } = const {
api.previewDeployment.all.useQuery( data: previewDeployments,
{ applicationId }, refetch: refetchPreviewDeployments,
{ isLoading: isLoadingPreviewDeployments,
enabled: !!applicationId, } = api.previewDeployment.all.useQuery(
}, { applicationId },
); {
enabled: !!applicationId,
},
);
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
deletePreviewDeployment({ deletePreviewDeployment({
@@ -80,8 +83,15 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
each pull request you create. each pull request you create.
</span> </span>
</div> </div>
{!previewDeployments?.length ? ( {isLoadingPreviewDeployments ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10"> <div className="flex w-full flex-row items-center justify-center gap-3 min-h-[35vh]">
<Loader2 className="size-5 text-muted-foreground animate-spin" />
<span className="text-base text-muted-foreground">
Loading preview deployments...
</span>
</div>
) : !previewDeployments?.length ? (
<div className="flex w-full flex-col items-center justify-center gap-3 min-h-[35vh]">
<RocketIcon className="size-8 text-muted-foreground" /> <RocketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
No preview deployments found No preview deployments found
@@ -168,19 +178,10 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
</Button> </Button>
</ShowModalLogs> </ShowModalLogs>
<ShowPreviewBuilds <ShowDeploymentsModal
deployments={deployment.deployments || []} id={deployment.previewDeploymentId}
type="previewDeployment"
serverId={data?.serverId || ""} serverId={data?.serverId || ""}
trigger={
<Button
variant="outline"
size="sm"
className="gap-2"
>
<Layers className="size-4" />
Builds
</Button>
}
/> />
<AddPreviewDomain <AddPreviewDomain

View File

@@ -140,8 +140,6 @@ const RestoreBackupSchema = z
}); });
} }
console.log(data.backupType, { metadata: data.metadata });
if (data.backupType === "compose" && !data.metadata?.serviceName) { if (data.backupType === "compose" && !data.metadata?.serviceName) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
@@ -532,7 +530,11 @@ export const RestoreBackup = ({
<FormItem> <FormItem>
<FormLabel>Database Name</FormLabel> <FormLabel>Database Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Enter database name" {...field} /> <Input
placeholder="Enter database name"
{...field}
disabled={databaseType === "web-server"}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -789,10 +791,10 @@ export const RestoreBackup = ({
isLoading={isDeploying} isLoading={isDeploying}
form="hook-form-restore-backup" form="hook-form-restore-backup"
type="submit" type="submit"
disabled={ // disabled={
!form.watch("backupFile") || // !form.watch("backupFile") ||
(backupType === "compose" && !form.watch("databaseType")) // (backupType === "compose" && !form.watch("databaseType"))
} // }
> >
Restore Restore
</Button> </Button>

View File

@@ -42,35 +42,35 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
); );
if (!containerId) { if (!containerId) {
writeStream.write("PostgreSQL container not found❌"); writeStream.write("Dokploy postgres container not found❌\n");
writeStream.end(); writeStream.end();
throw new Error("PostgreSQL container not found"); throw new Error("Dokploy postgres container not found");
} }
writeStream.write(`PostgreSQL container ID: ${containerId}`); writeStream.write(`Dokploy postgres container ID: ${containerId}\n`);
const postgresContainerId = containerId.trim(); const postgresContainerId = containerId.trim();
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`; const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
writeStream.write(`Running command: ${postgresCommand}`); writeStream.write(`Running command: ${postgresCommand}\n`);
await execAsync(postgresCommand); await execAsync(postgresCommand);
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);
writeStream.write("Copied filesystem to temp directory"); writeStream.write("Copied filesystem to temp directory\n");
await execAsync( await execAsync(
// Zip all .sql files since we created more than one // Zip all .sql files since we created more than one
`cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`, `cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`,
); );
writeStream.write("Zipped database and filesystem"); writeStream.write("Zipped database and filesystem\n");
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`; const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
writeStream.write(`Running command: ${uploadCommand}`); writeStream.write(`Running command: ${uploadCommand}\n`);
await execAsync(uploadCommand); await execAsync(uploadCommand);
writeStream.write("Uploaded backup to S3 ✅"); writeStream.write("Uploaded backup to S3 ✅\n");
writeStream.end(); writeStream.end();
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
return true; return true;
@@ -80,7 +80,9 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
} catch (error) { } catch (error) {
console.error("Backup error:", error); console.error("Backup error:", error);
writeStream.write("Backup error❌\n"); writeStream.write("Backup error❌\n");
writeStream.write(error instanceof Error ? error.message : "Unknown error"); writeStream.write(
error instanceof Error ? error.message : "Unknown error\n",
);
writeStream.end(); writeStream.end();
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
throw error; throw error;