feat: add domains

This commit is contained in:
Mauricio Siu 2025-01-18 22:55:35 -06:00
parent 08ab18eebf
commit 87546b4558
12 changed files with 406 additions and 361 deletions

View File

@ -1,90 +0,0 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import ReactMarkdown from "react-markdown";
import type { StepProps } from "./step-two";
export const StepFour = ({
prevStep,
templateInfo,
setOpen,
setTemplateInfo,
}: StepProps) => {
const handleSubmit = () => {
setTemplateInfo(templateInfo); // Update the template info
setOpen(false);
};
return (
<div className="flex flex-col h-full">
<div className="flex-grow">
<div className="space-y-6">
<h2 className="text-lg font-semibold">Step 4: Review and Finalize</h2>
<ScrollArea className="h-[400px] p-5">
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold">Name</h3>
<p className="text-sm text-muted-foreground">
{templateInfo?.details?.name}
</p>
</div>
<div>
<h3 className="text-sm font-semibold">Description</h3>
<ReactMarkdown className="text-sm text-muted-foreground">
{templateInfo?.details?.description}
</ReactMarkdown>
</div>
<div>
<h3 className="text-md font-semibold">Server</h3>
<p className="text-sm text-muted-foreground">
{templateInfo?.serverId || "Dokploy Server"}
</p>
</div>
<div>
<h3 className="text-sm font-semibold">Docker Compose</h3>
<CodeEditor
lineWrapping
value={templateInfo?.details?.dockerCompose}
disabled
className="font-mono"
/>
</div>
<div>
<h3 className="text-sm font-semibold">Environment Variables</h3>
<ul className="list-disc pl-5">
{templateInfo?.details?.envVariables.map(
(
env: {
name: string;
value: string;
},
index: number,
) => (
<li key={index}>
<strong className="text-sm font-semibold">
{env.name}
</strong>
:
<span className="text-sm ml-2 text-muted-foreground">
{env.value}
</span>
</li>
),
)}
</ul>
</div>
</div>
</ScrollArea>
</div>
</div>
<div className="sticky bottom-0 bg-background pt-2 border-t">
<div className="flex justify-between">
<Button onClick={prevStep} variant="outline">
Back
</Button>
<Button onClick={handleSubmit}>Create</Button>
</div>
</div>
</div>
);
};

View File

@ -3,7 +3,17 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useState } from "react"; import { useState } from "react";
import { api } from "@/utils/api";
const examples = [ const examples = [
"Make a personal blog", "Make a personal blog",
@ -16,8 +26,14 @@ const examples = [
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => { export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
const [userInput, setUserInput] = useState(templateInfo.userInput); const [userInput, setUserInput] = useState(templateInfo.userInput);
// Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery();
const handleNext = () => { const handleNext = () => {
setTemplateInfo({ ...templateInfo, userInput }); setTemplateInfo({
...templateInfo,
userInput,
});
nextStep(); nextStep();
}; };
@ -26,9 +42,9 @@ export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
}; };
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full gap-4">
<div className="flex-grow overflow-auto"> <div className="">
<div className="space-y-4 pb-20"> <div className="space-y-4 ">
<h2 className="text-lg font-semibold">Step 1: Describe Your Needs</h2> <h2 className="text-lg font-semibold">Step 1: Describe Your Needs</h2>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="user-needs">Describe your template needs</Label> <Label htmlFor="user-needs">Describe your template needs</Label>
@ -40,6 +56,39 @@ export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
className="min-h-[100px]" className="min-h-[100px]"
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="server-deploy">
Select the server where you want to deploy (optional)
</Label>
<Select
value={templateInfo.server?.serverId}
onValueChange={(value) => {
const server = servers?.find((s) => s.serverId === value);
if (server) {
setTemplateInfo({
...templateInfo,
server: server,
});
}
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem key={server.serverId} value={server.serverId}>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Examples:</Label> <Label>Examples:</Label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@ -57,7 +106,7 @@ export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
</div> </div>
</div> </div>
</div> </div>
<div className="sticky bottom-0 bg-background pt-2 border-t"> <div className="">
<div className="flex justify-end"> <div className="flex justify-end">
<Button onClick={handleNext} disabled={!userInput.trim()}> <Button onClick={handleNext} disabled={!userInput.trim()}>
Next Next

View File

@ -1,93 +1,104 @@
"use client"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import ReactMarkdown from "react-markdown";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { useState } from "react";
import type { StepProps } from "./step-two"; import type { StepProps } from "./step-two";
export const StepThree = ({ export const StepThree = ({
nextStep,
prevStep, prevStep,
templateInfo, templateInfo,
setTemplateInfo, onSubmit,
}: StepProps) => { }: StepProps & { onSubmit: () => void }) => {
const [server, setServer] = useState(templateInfo.serverId);
const { data: servers } = api.server.withSSHKey.useQuery();
const handleNext = () => {
const updatedInfo = { ...templateInfo, name };
if (server?.trim()) {
updatedInfo.serverId = server;
}
setTemplateInfo(updatedInfo);
nextStep();
};
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex-grow overflow-auto"> <div className="flex-grow">
<div className="space-y-4 pb-20"> <div className="space-y-6">
<h2 className="text-lg font-semibold">Step 3: Additional Details</h2> <h2 className="text-lg font-semibold">Step 3: Review and Finalize</h2>
<div> <div className="space-y-4">
<Label htmlFor="name">App Name</Label> <div>
<Input <h3 className="text-sm font-semibold">Name</h3>
id="name" <p className="text-sm text-muted-foreground">
value={templateInfo?.details?.name || ""} {templateInfo?.details?.name}
onChange={(e) => { </p>
setTemplateInfo({ </div>
...templateInfo, <div>
details: { <h3 className="text-sm font-semibold">Description</h3>
...templateInfo?.details, <ReactMarkdown className="text-sm text-muted-foreground">
name: e.target.value, {templateInfo?.details?.description}
}, </ReactMarkdown>
}); </div>
}} <div>
placeholder="Enter app name" <h3 className="text-md font-semibold">Server</h3>
className="mt-1" <p className="text-sm text-muted-foreground">
/> {templateInfo?.server?.name || "Dokploy Server"}
</div> </p>
<div> </div>
<Label htmlFor="server">Server Attachment (Optional)</Label> <div className="space-y-2">
<Select value={server} onValueChange={setServer}> <h3 className="text-sm font-semibold">Docker Compose</h3>
<SelectTrigger> <CodeEditor
<SelectValue placeholder="Select a Server" /> lineWrapping
</SelectTrigger> value={templateInfo?.details?.dockerCompose}
<SelectContent> disabled
<SelectGroup> className="font-mono"
{servers?.map((server) => ( />
<SelectItem key={server.serverId} value={server.serverId}> </div>
{server.name} <div>
</SelectItem> <h3 className="text-sm font-semibold">Environment Variables</h3>
))} <ul className="list-disc pl-5">
<SelectLabel>Servers ({servers?.length})</SelectLabel> {templateInfo?.details?.envVariables.map(
</SelectGroup> (
</SelectContent> env: {
</Select> name: string;
value: string;
},
index: number,
) => (
<li key={index}>
<strong className="text-sm font-semibold">
{env.name}
</strong>
:
<span className="text-sm ml-2 text-muted-foreground">
{env.value}
</span>
</li>
),
)}
</ul>
</div>
<div>
<h3 className="text-sm font-semibold">Domains</h3>
<ul className="list-disc pl-5">
{templateInfo?.details?.domains.map(
(
domain: {
host: string;
port: number;
serviceName: string;
},
index: number,
) => (
<li key={index}>
<strong className="text-sm font-semibold">
{domain.host}
</strong>
:
<span className="text-sm ml-2 text-muted-foreground">
{domain.port} - {domain.serviceName}
</span>
</li>
),
)}
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="sticky bottom-0 bg-background pt-2 border-t"> <div className="pt-6">
<div className="flex justify-between"> <div className="flex justify-between">
<Button onClick={prevStep} variant="outline"> <Button onClick={prevStep} variant="outline">
Back Back
</Button> </Button>
<Button <Button onClick={onSubmit}>Create</Button>
onClick={handleNext}
disabled={!templateInfo?.details?.name?.trim()}
>
Next
</Button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -40,7 +40,8 @@ export const StepTwo = ({
useEffect(() => { useEffect(() => {
mutateAsync({ mutateAsync({
aiId: templateInfo.aiId, aiId: templateInfo.aiId,
prompt: templateInfo.userInput, serverId: templateInfo.server?.serverId || "",
input: templateInfo.userInput,
}) })
.then((data) => { .then((data) => {
console.log(data); console.log(data);
@ -62,59 +63,98 @@ export const StepTwo = ({
nextStep(); nextStep();
}; };
const handleEnvVariableChange = (
index: number,
field: string,
value: string,
) => {
// const updatedEnvVariables = [...envVariables];
// if (updatedEnvVariables[index]) {
// updatedEnvVariables[index] = {
// ...updatedEnvVariables[index],
// [field]: value,
// };
// setEnvVariables(updatedEnvVariables);
// }
};
// const addEnvVariable = () => {
// setEnvVariables([...envVariables, { name: "", value: "" }]);
// setShowValues((prev) => ({ ...prev, "": false }));
// };
// const removeEnvVariable = (index: number) => {
// const updatedEnvVariables = envVariables.filter((_, i) => i !== index);
// setEnvVariables(updatedEnvVariables);
// };
// const handleDomainChange = (
// index: number,
// field: string,
// value: string | number,
// ) => {
// const updatedDomains = [...domains];
// if (updatedDomains[index]) {
// updatedDomains[index] = {
// ...updatedDomains[index],
// [field]: value,
// };
// setDomains(updatedDomains);
// }
// };
// const addDomain = () => {
// setDomains([...domains, { host: "", port: 0, serviceName: "" }]);
// };
// const removeDomain = (index: number) => {
// const updatedDomains = domains.filter((_, i) => i !== index);
// setDomains(updatedDomains);
// };
const toggleShowValue = (name: string) => { const toggleShowValue = (name: string) => {
setShowValues((prev) => ({ ...prev, [name]: !prev[name] })); setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
}; };
const handleEnvVariableChange = (
index: number,
field: "name" | "value",
value: string,
) => {
if (!selectedVariant) return;
const updatedEnvVariables = [...selectedVariant.envVariables];
updatedEnvVariables[index] = {
...updatedEnvVariables[index],
[field]: value,
};
setSelectedVariant({
...selectedVariant,
envVariables: updatedEnvVariables,
});
};
const removeEnvVariable = (index: number) => {
if (!selectedVariant) return;
const updatedEnvVariables = selectedVariant.envVariables.filter(
(_, i) => i !== index,
);
setSelectedVariant({
...selectedVariant,
envVariables: updatedEnvVariables,
});
};
const handleDomainChange = (
index: number,
field: "host" | "port" | "serviceName",
value: string | number,
) => {
if (!selectedVariant) return;
const updatedDomains = [...selectedVariant.domains];
updatedDomains[index] = {
...updatedDomains[index],
[field]: value,
};
setSelectedVariant({
...selectedVariant,
domains: updatedDomains,
});
};
const removeDomain = (index: number) => {
if (!selectedVariant) return;
const updatedDomains = selectedVariant.domains.filter(
(_, i) => i !== index,
);
setSelectedVariant({
...selectedVariant,
domains: updatedDomains,
});
};
const addEnvVariable = () => {
if (!selectedVariant) return;
setSelectedVariant({
...selectedVariant,
envVariables: [
...selectedVariant.envVariables,
{ name: "", value: "" },
],
});
};
const addDomain = () => {
if (!selectedVariant) return;
setSelectedVariant({
...selectedVariant,
domains: [
...selectedVariant.domains,
{ host: "", port: 80, serviceName: "" },
],
});
};
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex flex-col items-center justify-center h-full space-y-4"> <div className="flex flex-col items-center justify-center h-full space-y-4">
@ -131,7 +171,7 @@ export const StepTwo = ({
} }
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full gap-6">
<div className="flex-grow overflow-auto pb-8"> <div className="flex-grow overflow-auto pb-8">
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2> <h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2>
@ -207,7 +247,7 @@ export const StepTwo = ({
<AccordionItem value="env-variables"> <AccordionItem value="env-variables">
<AccordionTrigger>Environment Variables</AccordionTrigger> <AccordionTrigger>Environment Variables</AccordionTrigger>
<AccordionContent> <AccordionContent>
<ScrollArea className="h-[300px] w-full rounded-md border"> <ScrollArea className=" w-full rounded-md border">
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
{selectedVariant?.envVariables.map((env, index) => ( {selectedVariant?.envVariables.map((env, index) => (
<div <div
@ -270,7 +310,7 @@ export const StepTwo = ({
variant="outline" variant="outline"
size="sm" size="sm"
className="mt-2" className="mt-2"
// onClick={addEnvVariable} onClick={addEnvVariable}
> >
<PlusCircle className="h-4 w-4 mr-2" /> <PlusCircle className="h-4 w-4 mr-2" />
Add Variable Add Variable
@ -308,7 +348,7 @@ export const StepTwo = ({
handleDomainChange( handleDomainChange(
index, index,
"port", "port",
parseInt(e.target.value), Number.parseInt(e.target.value),
) )
} }
placeholder="Port" placeholder="Port"
@ -341,7 +381,7 @@ export const StepTwo = ({
variant="outline" variant="outline"
size="sm" size="sm"
className="mt-2" className="mt-2"
// onClick={addDomain} onClick={addDomain}
> >
<PlusCircle className="h-4 w-4 mr-2" /> <PlusCircle className="h-4 w-4 mr-2" />
Add Domain Add Domain
@ -356,7 +396,7 @@ export const StepTwo = ({
)} )}
</div> </div>
</div> </div>
<div className="sticky bottom-0 bg-background pt-5 border-t"> <div className="">
<div className="flex justify-between"> <div className="flex justify-between">
<Button <Button
onClick={() => onClick={() =>

View File

@ -1,4 +1,4 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { AlertBlock } from "@/components/shared/alert-block";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -17,14 +17,12 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Bot } from "lucide-react"; import { Bot } from "lucide-react";
import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { StepFour } from "./step-four";
import { StepOne } from "./step-one"; import { StepOne } from "./step-one";
import { StepThree } from "./step-three"; import { StepThree } from "./step-three";
import { StepTwo } from "./step-two"; import { StepTwo } from "./step-two";
import { AlertBlock } from "@/components/shared/alert-block";
import Link from "next/link";
interface EnvVariable { interface EnvVariable {
name: string; name: string;
@ -37,9 +35,7 @@ interface Domain {
serviceName: string; serviceName: string;
} }
export interface TemplateInfo { export interface TemplateInfo {
id: string;
userInput: string; userInput: string;
type: string;
details?: { details?: {
name: string; name: string;
id: string; id: string;
@ -49,15 +45,17 @@ export interface TemplateInfo {
shortDescription: string; shortDescription: string;
domains: Domain[]; domains: Domain[];
}; };
serverId?: string; server?: {
serverId: string;
name: string;
};
aiId: string; aiId: string;
} }
const defaultTemplateInfo: TemplateInfo = { const defaultTemplateInfo: TemplateInfo = {
id: "",
aiId: "", aiId: "",
userInput: "", userInput: "",
type: "", server: undefined,
details: { details: {
id: "", id: "",
name: "", name: "",
@ -83,7 +81,7 @@ export const TemplateGenerator = ({ projectId }: Props) => {
useState<TemplateInfo>(defaultTemplateInfo); useState<TemplateInfo>(defaultTemplateInfo);
const utils = api.useUtils(); const utils = api.useUtils();
const totalSteps = 4; const totalSteps = 3;
const nextStep = () => setStep((prev) => Math.min(prev + 1, totalSteps)); const nextStep = () => setStep((prev) => Math.min(prev + 1, totalSteps));
const prevStep = () => setStep((prev) => Math.max(prev - 1, 1)); const prevStep = () => setStep((prev) => Math.max(prev - 1, 1));
@ -118,7 +116,7 @@ export const TemplateGenerator = ({ projectId }: Props) => {
Create a custom template based on your needs Create a custom template based on your needs
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="mt-4 flex-grow overflow-auto"> <div className="mt-4 ">
{step === 1 && ( {step === 1 && (
<> <>
{!haveAtleasOneProviderEnabled && ( {!haveAtleasOneProviderEnabled && (
@ -192,29 +190,25 @@ export const TemplateGenerator = ({ projectId }: Props) => {
)} )}
{step === 3 && ( {step === 3 && (
<StepThree <StepThree
prevStep={prevStep}
templateInfo={templateInfo}
nextStep={nextStep} nextStep={nextStep}
prevStep={prevStep}
templateInfo={templateInfo}
setTemplateInfo={setTemplateInfo} setTemplateInfo={setTemplateInfo}
/> onSubmit={async () => {
)} console.log("Submitting template:", templateInfo);
{step === 4 && (
<StepFour
prevStep={prevStep}
templateInfo={templateInfo}
setTemplateInfo={async (data: any) => {
console.log("Submitting template:", data);
setTemplateInfo(data);
await mutateAsync({ await mutateAsync({
projectId, projectId,
id: templateInfo.details?.id || "", id: templateInfo.details?.id || "",
name: templateInfo?.details?.name || "", name: templateInfo?.details?.name || "",
description: data?.details?.shortDescription || "", description: templateInfo?.details?.shortDescription || "",
dockerCompose: data?.details?.dockerCompose || "", dockerCompose: templateInfo?.details?.dockerCompose || "",
envVariables: (data?.details?.envVariables || []) envVariables: (templateInfo?.details?.envVariables || [])
.map((env: any) => `${env.name}=${env.value}`) .map((env: any) => `${env.name}=${env.value}`)
.join("\n"), .join("\n"),
serverId: data.server || "", domains: templateInfo?.details?.domains || [],
...(templateInfo.server?.serverId && {
serverId: templateInfo.server?.serverId || "",
}),
}) })
.then(async () => { .then(async () => {
toast.success("Compose Created"); toast.success("Compose Created");
@ -227,7 +221,6 @@ export const TemplateGenerator = ({ projectId }: Props) => {
toast.error("Error creating the compose"); toast.error("Error creating the compose");
}); });
}} }}
setOpen={setOpen}
/> />
)} )}
</div> </div>

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@ -12,7 +13,6 @@ import { api } from "@/utils/api";
import { BotIcon, Loader2, Trash2 } from "lucide-react"; import { BotIcon, Loader2, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { HandleAi } from "./handle-ai"; import { HandleAi } from "./handle-ai";
import { DialogAction } from "@/components/shared/dialog-action";
export const AiForm = () => { export const AiForm = () => {
const { data: aiConfigs, refetch, isLoading } = api.ai.getAll.useQuery(); const { data: aiConfigs, refetch, isLoading } = api.ai.getAll.useQuery();

View File

@ -1,5 +1,14 @@
"use client"; "use client";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { import {
Form, Form,
FormControl, FormControl,
@ -18,22 +27,13 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useEffect, useState } from "react";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { message: "Name is required" }), name: z.string().min(1, { message: "Name is required" }),

View File

@ -7,6 +7,7 @@ import {
Bell, Bell,
BlocksIcon, BlocksIcon,
BookIcon, BookIcon,
BotIcon,
Boxes, Boxes,
ChevronRight, ChevronRight,
CircleHelp, CircleHelp,
@ -19,7 +20,6 @@ import {
GitBranch, GitBranch,
HeartIcon, HeartIcon,
KeyRound, KeyRound,
BotIcon,
type LucideIcon, type LucideIcon,
Package, Package,
PieChart, PieChart,

View File

@ -11,11 +11,12 @@ import {
apiUpdateAi, apiUpdateAi,
deploySuggestionSchema, deploySuggestionSchema,
} from "@dokploy/server/db/schema/ai"; } from "@dokploy/server/db/schema/ai";
import { createDomain } from "@dokploy/server/index";
import { import {
getAiSettingsByAdminId,
getAiSettingById,
saveAiSettings,
deleteAiSettings, deleteAiSettings,
getAiSettingById,
getAiSettingsByAdminId,
saveAiSettings,
suggestVariants, suggestVariants,
} from "@dokploy/server/services/ai"; } from "@dokploy/server/services/ai";
import { createComposeByTemplate } from "@dokploy/server/services/compose"; import { createComposeByTemplate } from "@dokploy/server/services/compose";
@ -84,11 +85,15 @@ export const aiRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
aiId: z.string(), aiId: z.string(),
prompt: z.string(), input: z.string(),
serverId: z.string().optional(),
}) })
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
return await suggestVariants(ctx.user.adminId, input.aiId, input.prompt); return await suggestVariants({
...input,
adminId: ctx.user.adminId,
});
}), }),
deploy: protectedProcedure deploy: protectedProcedure
.input(deploySuggestionSchema) .input(deploySuggestionSchema)
@ -108,6 +113,8 @@ export const aiRouter = createTRPCRouter({
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
console.log(input);
const compose = await createComposeByTemplate({ const compose = await createComposeByTemplate({
...input, ...input,
composeFile: input.dockerCompose, composeFile: input.dockerCompose,
@ -118,6 +125,17 @@ export const aiRouter = createTRPCRouter({
appName: `${projectName}-${generatePassword(6)}`, appName: `${projectName}-${generatePassword(6)}`,
}); });
if (input.domains && input.domains?.length > 0) {
for (const domain of input.domains) {
await createDomain({
...domain,
domainType: "compose",
certificateType: "none",
composeId: compose.composeId,
});
}
}
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, compose.composeId); await addNewService(ctx.user.authId, compose.composeId);
} }

View File

@ -3,119 +3,119 @@ import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ai } from "./ai";
import { auth } from "./auth"; import { auth } from "./auth";
import { certificates } from "./certificate"; import { certificates } from "./certificate";
import { registry } from "./registry"; import { registry } from "./registry";
import { certificateType } from "./shared"; import { certificateType } from "./shared";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { users } from "./user"; import { users } from "./user";
import { ai } from "./ai";
export const admins = pgTable("admin", { export const admins = pgTable("admin", {
adminId: text("adminId") adminId: text("adminId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
serverIp: text("serverIp"), serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"), certificateType: certificateType("certificateType").notNull().default("none"),
host: text("host"), host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"), letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"), sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false), enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
enableLogRotation: boolean("enableLogRotation").notNull().default(false), enableLogRotation: boolean("enableLogRotation").notNull().default(false),
authId: text("authId") authId: text("authId")
.notNull() .notNull()
.references(() => auth.id, { onDelete: "cascade" }), .references(() => auth.id, { onDelete: "cascade" }),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
stripeCustomerId: text("stripeCustomerId"), stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"), stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0), serversQuantity: integer("serversQuantity").notNull().default(0),
}); });
export const adminsRelations = relations(admins, ({ one, many }) => ({ export const adminsRelations = relations(admins, ({ one, many }) => ({
auth: one(auth, { auth: one(auth, {
fields: [admins.authId], fields: [admins.authId],
references: [auth.id], references: [auth.id],
}), }),
users: many(users), users: many(users),
registry: many(registry), registry: many(registry),
sshKeys: many(sshKeys), sshKeys: many(sshKeys),
certificates: many(certificates), certificates: many(certificates),
ai: many(ai), ai: many(ai),
})); }));
const createSchema = createInsertSchema(admins, { const createSchema = createInsertSchema(admins, {
adminId: z.string(), adminId: z.string(),
enableDockerCleanup: z.boolean().optional(), enableDockerCleanup: z.boolean().optional(),
sshPrivateKey: z.string().optional(), sshPrivateKey: z.string().optional(),
certificateType: z.enum(["letsencrypt", "none"]).default("none"), certificateType: z.enum(["letsencrypt", "none"]).default("none"),
serverIp: z.string().optional(), serverIp: z.string().optional(),
letsEncryptEmail: z.string().optional(), letsEncryptEmail: z.string().optional(),
}); });
export const apiUpdateAdmin = createSchema.partial(); export const apiUpdateAdmin = createSchema.partial();
export const apiSaveSSHKey = createSchema export const apiSaveSSHKey = createSchema
.pick({ .pick({
sshPrivateKey: true, sshPrivateKey: true,
}) })
.required(); .required();
export const apiAssignDomain = createSchema export const apiAssignDomain = createSchema
.pick({ .pick({
host: true, host: true,
certificateType: true, certificateType: true,
letsEncryptEmail: true, letsEncryptEmail: true,
}) })
.required() .required()
.partial({ .partial({
letsEncryptEmail: true, letsEncryptEmail: true,
}); });
export const apiUpdateDockerCleanup = createSchema export const apiUpdateDockerCleanup = createSchema
.pick({ .pick({
enableDockerCleanup: true, enableDockerCleanup: true,
}) })
.required() .required()
.extend({ .extend({
serverId: z.string().optional(), serverId: z.string().optional(),
}); });
export const apiTraefikConfig = z.object({ export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1), traefikConfig: z.string().min(1),
}); });
export const apiModifyTraefikConfig = z.object({ export const apiModifyTraefikConfig = z.object({
path: z.string().min(1), path: z.string().min(1),
traefikConfig: z.string().min(1), traefikConfig: z.string().min(1),
serverId: z.string().optional(), serverId: z.string().optional(),
}); });
export const apiReadTraefikConfig = z.object({ export const apiReadTraefikConfig = z.object({
path: z.string().min(1), path: z.string().min(1),
serverId: z.string().optional(), serverId: z.string().optional(),
}); });
export const apiEnableDashboard = z.object({ export const apiEnableDashboard = z.object({
enableDashboard: z.boolean().optional(), enableDashboard: z.boolean().optional(),
serverId: z.string().optional(), serverId: z.string().optional(),
}); });
export const apiServerSchema = z export const apiServerSchema = z
.object({ .object({
serverId: z.string().optional(), serverId: z.string().optional(),
}) })
.optional(); .optional();
export const apiReadStatsLogs = z.object({ export const apiReadStatsLogs = z.object({
page: z page: z
.object({ .object({
pageIndex: z.number(), pageIndex: z.number(),
pageSize: z.number(), pageSize: z.number(),
}) })
.optional(), .optional(),
status: z.string().array().optional(), status: z.string().array().optional(),
search: z.string().optional(), search: z.string().optional(),
sort: z.object({ id: z.string(), desc: z.boolean() }).optional(), sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
}); });

View File

@ -1,9 +1,9 @@
import { relations } from "drizzle-orm";
import { boolean, pgTable, text } from "drizzle-orm/pg-core"; import { boolean, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { admins } from "./admin"; import { admins } from "./admin";
import { relations } from "drizzle-orm";
import { nanoid } from "nanoid";
export const ai = pgTable("ai", { export const ai = pgTable("ai", {
aiId: text("aiId") aiId: text("aiId")
@ -63,4 +63,13 @@ export const deploySuggestionSchema = z.object({
serverId: z.string().optional(), serverId: z.string().optional(),
name: z.string().min(1), name: z.string().min(1),
description: z.string(), description: z.string(),
domains: z
.array(
z.object({
host: z.string().min(1),
port: z.number().min(1),
serviceName: z.string().min(1),
})
)
.optional(),
}); });

View File

@ -7,6 +7,7 @@ import { desc, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { findAdminById } from "./admin"; import { findAdminById } from "./admin";
import { findServerById } from "./server";
export const getAiSettingsByAdminId = async (adminId: string) => { export const getAiSettingsByAdminId = async (adminId: string) => {
const aiSettings = await db.query.ai.findMany({ const aiSettings = await db.query.ai.findMany({
@ -51,11 +52,19 @@ export const deleteAiSettings = async (aiId: string) => {
return db.delete(ai).where(eq(ai.aiId, aiId)); return db.delete(ai).where(eq(ai.aiId, aiId));
}; };
export const suggestVariants = async ( interface Props {
adminId: string, adminId: string;
aiId: string, aiId: string;
input: string input: string;
) => { serverId?: string | undefined;
}
export const suggestVariants = async ({
adminId,
aiId,
input,
serverId,
}: Props) => {
try { try {
const aiSettings = await getAiSettingById(aiId); const aiSettings = await getAiSettingById(aiId);
if (!aiSettings || !aiSettings.isEnabled) { if (!aiSettings || !aiSettings.isEnabled) {
@ -74,6 +83,13 @@ export const suggestVariants = async (
ip = admin?.serverIp || ""; ip = admin?.serverIp || "";
} }
if (serverId) {
const server = await findServerById(serverId);
ip = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
ip = "127.0.0.1";
}
const { object } = await generateObject({ const { object } = await generateObject({
model, model,
output: "array", output: "array",
@ -122,13 +138,13 @@ export const suggestVariants = async (
2. Use complex values for passwords/secrets variables 2. Use complex values for passwords/secrets variables
3. Don't set container_name field in services 3. Don't set container_name field in services
4. Don't set version field in the docker compose 4. Don't set version field in the docker compose
5. Don't set ports like 'ports: 3000:3000', use 'ports: ["3000"]' instead 5. Don't set ports like 'ports: 3000:3000', use 'ports: "3000"' instead
6. Use dokploy-network in all services 6. Use dokploy-network in all services
7. Add dokploy-network at the end and mark it as external: true 7. Add dokploy-network at the end and mark it as external: true
For each service that needs to be exposed to the internet: For each service that needs to be exposed to the internet:
1. Define a domain configuration with: 1. Define a domain configuration with:
- host: the domain name for the service - host: the domain name for the service in format: {service-name}-{random-3-chars-hex}-${ip ? ip.replaceAll(".", "-") : ""}.traefik.me
- port: the internal port the service runs on - port: the internal port the service runs on
- serviceName: the name of the service in the docker-compose - serviceName: the name of the service in the docker-compose
2. Make sure the service is properly configured in the docker-compose to work with the specified port 2. Make sure the service is properly configured in the docker-compose to work with the specified port
@ -145,7 +161,6 @@ export const suggestVariants = async (
} }
} catch (error) { } catch (error) {
console.error("Error in docker compose generation:", error); console.error("Error in docker compose generation:", error);
console.error("Error details:", error.cause?.issues || error);
} }
} }
return result; return result;