feat(isolation): add preview functionality for isolated deployment with loading state and dialog

This commit is contained in:
Mauricio Siu
2025-08-10 16:38:10 -06:00
parent 4b1146ab6b
commit 1fe12ba93e
3 changed files with 107 additions and 36 deletions

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { AlertTriangle, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -14,6 +14,13 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
@@ -22,7 +29,7 @@ import {
FormItem,
FormLabel,
} from "@/components/ui/form";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
@@ -40,9 +47,12 @@ type IsolatedSchema = z.infer<typeof isolatedSchema>;
export const IsolatedDeploymentTab = ({ composeId }: Props) => {
const utils = api.useUtils();
const [compose, setCompose] = useState<string>("");
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(false);
const { mutateAsync, error, isError } =
api.compose.isolatedDeployment.useMutation();
const [isOpenPreview, setIsOpenPreview] = useState<boolean>(false);
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
const { data, refetch } = api.compose.one.useQuery(
@@ -58,7 +68,6 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
});
useEffect(() => {
randomizeCompose();
if (data) {
form.reset({
isolatedDeployment: data?.isolatedDeployment || false,
@@ -72,7 +81,6 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
isolatedDeployment: formData?.isolatedDeployment || false,
})
.then(async (_data) => {
await randomizeCompose();
await refetch();
toast.success("Compose updated");
})
@@ -81,26 +89,31 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
});
};
const randomizeCompose = async () => {
await mutateAsync({
composeId,
suffix: data?.appName || "",
}).then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
});
const generatePreview = async () => {
setIsOpenPreview(true);
setIsPreviewLoading(true);
try {
await mutateAsync({
composeId,
suffix: data?.appName || "",
}).then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
});
} catch {
toast.error("Error generating preview");
setIsOpenPreview(false);
} finally {
setIsPreviewLoading(false);
}
};
return (
<Card className="bg-background">
<CardHeader>
<CardTitle>Enable Isolated Deployment</CardTitle>
<CardTitle className="text-xl">Enable Isolated Deployment</CardTitle>
<CardDescription>
Configure isolated deployment to the compose file.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="text-sm text-muted-foreground flex flex-col gap-2">
<span>
This feature creates an isolated environment for your deployment
@@ -122,6 +135,10 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
</div>
</div>
</div>
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
@@ -175,16 +192,46 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
</Button>
</div>
</div>
<div className="flex flex-col gap-4">
<Label>Preview</Label>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
<Button
onClick={generatePreview}
isLoading={isPreviewLoading}
variant="secondary"
className="lg:w-fit"
>
Preview Compose
</Button>
<Dialog open={isOpenPreview} onOpenChange={setIsOpenPreview}>
<DialogContent className="sm:max-w-6xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Isolated Deployment Preview</DialogTitle>
<DialogDescription>
Preview of the compose file with isolated deployment
configuration
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 overflow-auto">
{isPreviewLoading ? (
<div className="flex flex-col items-center justify-center py-12 gap-4">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
<p className="text-muted-foreground">
Generating compose preview...
</p>
</div>
) : (
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="60vh"
/>
</pre>
)}
</div>
</DialogContent>
</Dialog>
</div>
</form>
</Form>

View File

@@ -62,7 +62,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<AlertBlock type="info">
<AlertBlock type="info" className="mb-4">
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
</AlertBlock>

View File

@@ -1,8 +1,14 @@
import { findComposeById } from "@dokploy/server/services/compose";
import { dump, load } from "js-yaml";
import { dump } from "js-yaml";
import { addAppNameToAllServiceNames } from "./collision/root-network";
import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume";
import {
cloneCompose,
cloneComposeRemote,
loadDockerCompose,
loadDockerComposeRemote,
} from "./domain";
import type { ComposeSpecification } from "./types";
export const addAppNameToPreventCollision = (
@@ -24,16 +30,34 @@ export const randomizeIsolatedDeploymentComposeFile = async (
suffix?: string,
) => {
const compose = await findComposeById(composeId);
const composeFile = compose.composeFile;
const composeData = load(composeFile) as ComposeSpecification;
if (compose.serverId) {
await cloneComposeRemote(compose);
} else {
await cloneCompose(compose);
}
let composeData: ComposeSpecification | null;
if (compose.serverId) {
composeData = await loadDockerComposeRemote(compose);
} else {
composeData = await loadDockerCompose(compose);
}
if (!composeData) {
throw new Error("Compose data not found");
}
const randomSuffix = suffix || compose.appName || generateRandomHash();
const newComposeFile = addAppNameToPreventCollision(
composeData,
randomSuffix,
compose.isolatedDeploymentsVolume,
);
const newComposeFile = compose.isolatedDeployment
? addAppNameToPreventCollision(
composeData,
randomSuffix,
compose.isolatedDeploymentsVolume,
)
: composeData;
return dump(newComposeFile);
};