mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: use stepper
This commit is contained in:
@@ -24,23 +24,12 @@ const examples = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
|
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
|
||||||
const [userInput, setUserInput] = useState(templateInfo.userInput);
|
|
||||||
|
|
||||||
// Get servers from the API
|
// Get servers from the API
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
setTemplateInfo({
|
|
||||||
...templateInfo,
|
|
||||||
userInput,
|
|
||||||
});
|
|
||||||
nextStep();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExampleClick = (example: string) => {
|
const handleExampleClick = (example: string) => {
|
||||||
setUserInput(example);
|
setTemplateInfo({ ...templateInfo, userInput: example });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full gap-4">
|
<div className="flex flex-col h-full gap-4">
|
||||||
<div className="">
|
<div className="">
|
||||||
@@ -51,8 +40,10 @@ export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
|
|||||||
<Textarea
|
<Textarea
|
||||||
id="user-needs"
|
id="user-needs"
|
||||||
placeholder="Describe the type of template you need, its purpose, and any specific features you'd like to include."
|
placeholder="Describe the type of template you need, its purpose, and any specific features you'd like to include."
|
||||||
value={userInput}
|
value={templateInfo?.userInput}
|
||||||
onChange={(e) => setUserInput(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setTemplateInfo({ ...templateInfo, userInput: e.target.value })
|
||||||
|
}
|
||||||
className="min-h-[100px]"
|
className="min-h-[100px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,13 +97,6 @@ export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="">
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button onClick={handleNext} disabled={!userInput.trim()}>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import type { StepProps } from "./step-two";
|
import type { StepProps } from "./step-two";
|
||||||
|
|
||||||
export const StepThree = ({
|
export const StepThree = ({ templateInfo }: StepProps) => {
|
||||||
prevStep,
|
|
||||||
templateInfo,
|
|
||||||
onSubmit,
|
|
||||||
}: StepProps & { onSubmit: () => void }) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
@@ -93,14 +88,6 @@ export const StepThree = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-6">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<Button onClick={prevStep} variant="outline">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onSubmit}>Create</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -16,54 +17,45 @@ import { useEffect, useState } from "react";
|
|||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { TemplateInfo } from "./template-generator";
|
import type { TemplateInfo } from "./template-generator";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
|
|
||||||
export interface StepProps {
|
export interface StepProps {
|
||||||
nextStep: () => void;
|
stepper?: any;
|
||||||
prevStep: () => void;
|
|
||||||
templateInfo: TemplateInfo;
|
templateInfo: TemplateInfo;
|
||||||
setTemplateInfo: React.Dispatch<React.SetStateAction<TemplateInfo>>;
|
setTemplateInfo: React.Dispatch<React.SetStateAction<TemplateInfo>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StepTwo = ({
|
export const StepTwo = ({
|
||||||
nextStep,
|
stepper,
|
||||||
prevStep,
|
|
||||||
templateInfo,
|
templateInfo,
|
||||||
setTemplateInfo,
|
setTemplateInfo,
|
||||||
}: StepProps) => {
|
}: StepProps) => {
|
||||||
const [suggestions, setSuggestions] = useState<TemplateInfo["details"][]>();
|
const suggestions = templateInfo.suggestions || [];
|
||||||
const [selectedVariant, setSelectedVariant] =
|
const selectedVariant = templateInfo.details;
|
||||||
useState<TemplateInfo["details"]>();
|
|
||||||
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.ai.suggest.useMutation();
|
api.ai.suggest.useMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (suggestions?.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mutateAsync({
|
mutateAsync({
|
||||||
aiId: templateInfo.aiId,
|
aiId: templateInfo.aiId,
|
||||||
serverId: templateInfo.server?.serverId || "",
|
serverId: templateInfo.server?.serverId || "",
|
||||||
input: templateInfo.userInput,
|
input: templateInfo.userInput,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log(data);
|
setTemplateInfo({
|
||||||
setSuggestions(data);
|
...templateInfo,
|
||||||
|
suggestions: data,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error("Error generating suggestions");
|
toast.error("Error generating suggestions");
|
||||||
});
|
});
|
||||||
}, [templateInfo.userInput]);
|
}, [templateInfo.userInput]);
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
if (selectedVariant) {
|
|
||||||
setTemplateInfo({
|
|
||||||
...templateInfo,
|
|
||||||
details: selectedVariant,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
nextStep();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleShowValue = (name: string) => {
|
const toggleShowValue = (name: string) => {
|
||||||
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
|
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
|
||||||
};
|
};
|
||||||
@@ -82,9 +74,14 @@ export const StepTwo = ({
|
|||||||
[field]: value,
|
[field]: value,
|
||||||
};
|
};
|
||||||
|
|
||||||
setSelectedVariant({
|
setTemplateInfo({
|
||||||
...selectedVariant,
|
...templateInfo,
|
||||||
envVariables: updatedEnvVariables,
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
envVariables: updatedEnvVariables,
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,9 +92,14 @@ export const StepTwo = ({
|
|||||||
(_, i) => i !== index,
|
(_, i) => i !== index,
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedVariant({
|
setTemplateInfo({
|
||||||
...selectedVariant,
|
...templateInfo,
|
||||||
envVariables: updatedEnvVariables,
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
envVariables: updatedEnvVariables,
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,9 +116,14 @@ export const StepTwo = ({
|
|||||||
[field]: value,
|
[field]: value,
|
||||||
};
|
};
|
||||||
|
|
||||||
setSelectedVariant({
|
setTemplateInfo({
|
||||||
...selectedVariant,
|
...templateInfo,
|
||||||
domains: updatedDomains,
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
domains: updatedDomains,
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,30 +134,49 @@ export const StepTwo = ({
|
|||||||
(_, i) => i !== index,
|
(_, i) => i !== index,
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedVariant({
|
setTemplateInfo({
|
||||||
...selectedVariant,
|
...templateInfo,
|
||||||
domains: updatedDomains,
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
domains: updatedDomains,
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addEnvVariable = () => {
|
const addEnvVariable = () => {
|
||||||
if (!selectedVariant) return;
|
if (!selectedVariant) return;
|
||||||
|
|
||||||
setSelectedVariant({
|
setTemplateInfo({
|
||||||
...selectedVariant,
|
...templateInfo,
|
||||||
envVariables: [...selectedVariant.envVariables, { name: "", value: "" }],
|
|
||||||
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
envVariables: [
|
||||||
|
...selectedVariant.envVariables,
|
||||||
|
{ name: "", value: "" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addDomain = () => {
|
const addDomain = () => {
|
||||||
if (!selectedVariant) return;
|
if (!selectedVariant) return;
|
||||||
|
|
||||||
setSelectedVariant({
|
setTemplateInfo({
|
||||||
...selectedVariant,
|
...templateInfo,
|
||||||
domains: [
|
...(templateInfo.details && {
|
||||||
...selectedVariant.domains,
|
details: {
|
||||||
{ host: "", port: 80, serviceName: "" },
|
...templateInfo.details,
|
||||||
],
|
domains: [
|
||||||
|
...selectedVariant.domains,
|
||||||
|
{ host: "", port: 80, serviceName: "" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@@ -161,15 +187,6 @@ export const StepTwo = ({
|
|||||||
<AlertBlock type="error">
|
<AlertBlock type="error">
|
||||||
{error?.message || "Error generating suggestions"}
|
{error?.message || "Error generating suggestions"}
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
selectedVariant ? setSelectedVariant(undefined) : prevStep()
|
|
||||||
}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{selectedVariant ? "Change Variant" : "Back"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -197,10 +214,13 @@ export const StepTwo = ({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>Based on your input, we suggest the following variants:</div>
|
<div>Based on your input, we suggest the following variants:</div>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={selectedVariant}
|
// value={selectedVariant?.}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const element = suggestions?.find((s) => s?.id === value);
|
const element = suggestions?.find((s) => s?.id === value);
|
||||||
setSelectedVariant(element);
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
details: element,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
@@ -254,9 +274,14 @@ export const StepTwo = ({
|
|||||||
value={selectedVariant?.dockerCompose}
|
value={selectedVariant?.dockerCompose}
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelectedVariant({
|
setTemplateInfo({
|
||||||
...selectedVariant,
|
...templateInfo,
|
||||||
dockerCompose: value,
|
...(templateInfo?.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
dockerCompose: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -416,17 +441,17 @@ export const StepTwo = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<Button
|
{selectedVariant && (
|
||||||
onClick={() =>
|
<Button
|
||||||
selectedVariant ? setSelectedVariant(undefined) : prevStep()
|
onClick={() => {
|
||||||
}
|
const { details, ...rest } = templateInfo;
|
||||||
variant="outline"
|
setTemplateInfo(rest);
|
||||||
>
|
}}
|
||||||
{selectedVariant ? "Change Variant" : "Back"}
|
variant="outline"
|
||||||
</Button>
|
>
|
||||||
<Button onClick={handleNext} disabled={!selectedVariant}>
|
Change Variant
|
||||||
Next
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -18,11 +19,14 @@ import {
|
|||||||
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 Link from "next/link";
|
||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
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 { defineStepper } from "@stepperize/react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface EnvVariable {
|
interface EnvVariable {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -34,17 +38,20 @@ interface Domain {
|
|||||||
port: number;
|
port: number;
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Details {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
dockerCompose: string;
|
||||||
|
envVariables: EnvVariable[];
|
||||||
|
shortDescription: string;
|
||||||
|
domains: Domain[];
|
||||||
|
}
|
||||||
export interface TemplateInfo {
|
export interface TemplateInfo {
|
||||||
userInput: string;
|
userInput: string;
|
||||||
details?: {
|
details?: Details | null;
|
||||||
name: string;
|
suggestions?: Details[];
|
||||||
id: string;
|
|
||||||
description: string;
|
|
||||||
dockerCompose: string;
|
|
||||||
envVariables: EnvVariable[];
|
|
||||||
shortDescription: string;
|
|
||||||
domains: Domain[];
|
|
||||||
};
|
|
||||||
server?: {
|
server?: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -56,17 +63,25 @@ const defaultTemplateInfo: TemplateInfo = {
|
|||||||
aiId: "",
|
aiId: "",
|
||||||
userInput: "",
|
userInput: "",
|
||||||
server: undefined,
|
server: undefined,
|
||||||
details: {
|
details: null,
|
||||||
id: "",
|
suggestions: [],
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
dockerCompose: "",
|
|
||||||
envVariables: [],
|
|
||||||
shortDescription: "",
|
|
||||||
domains: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const { useStepper, steps, Scoped } = defineStepper(
|
||||||
|
{
|
||||||
|
id: "needs",
|
||||||
|
title: "Describe your needs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "variant",
|
||||||
|
title: "Choose a Variant",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "review",
|
||||||
|
title: "Review and Finalize",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
@@ -74,32 +89,58 @@ interface Props {
|
|||||||
|
|
||||||
export const TemplateGenerator = ({ projectId }: Props) => {
|
export const TemplateGenerator = ({ projectId }: Props) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [step, setStep] = useState(1);
|
const stepper = useStepper();
|
||||||
const { data: aiSettings } = api.ai.getAll.useQuery();
|
const { data: aiSettings } = api.ai.getAll.useQuery();
|
||||||
const { mutateAsync } = api.ai.deploy.useMutation();
|
const { mutateAsync } = api.ai.deploy.useMutation();
|
||||||
const [templateInfo, setTemplateInfo] =
|
const [templateInfo, setTemplateInfo] =
|
||||||
useState<TemplateInfo>(defaultTemplateInfo);
|
useState<TemplateInfo>(defaultTemplateInfo);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const totalSteps = 3;
|
|
||||||
|
|
||||||
const nextStep = () => setStep((prev) => Math.min(prev + 1, totalSteps));
|
|
||||||
const prevStep = () => setStep((prev) => Math.max(prev - 1, 1));
|
|
||||||
|
|
||||||
const handleOpenChange = (newOpen: boolean) => {
|
|
||||||
setOpen(newOpen);
|
|
||||||
if (!newOpen) {
|
|
||||||
setStep(1);
|
|
||||||
setTemplateInfo(defaultTemplateInfo);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const haveAtleasOneProviderEnabled = aiSettings?.some(
|
const haveAtleasOneProviderEnabled = aiSettings?.some(
|
||||||
(ai) => ai.isEnabled === true,
|
(ai) => ai.isEnabled === true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isDisabled = () => {
|
||||||
|
if (stepper.current.id === "needs") {
|
||||||
|
return !templateInfo.aiId || !templateInfo.userInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepper.current.id === "variant") {
|
||||||
|
return !templateInfo?.details?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
projectId,
|
||||||
|
id: templateInfo.details?.id || "",
|
||||||
|
name: templateInfo?.details?.name || "",
|
||||||
|
description: templateInfo?.details?.shortDescription || "",
|
||||||
|
dockerCompose: templateInfo?.details?.dockerCompose || "",
|
||||||
|
envVariables: (templateInfo?.details?.envVariables || [])
|
||||||
|
.map((env: any) => `${env.name}=${env.value}`)
|
||||||
|
.join("\n"),
|
||||||
|
domains: templateInfo?.details?.domains || [],
|
||||||
|
...(templateInfo.server?.serverId && {
|
||||||
|
serverId: templateInfo.server?.serverId || "",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Compose Created");
|
||||||
|
setOpen(false);
|
||||||
|
await utils.project.one.invalidate({
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error creating the compose");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild className="w-full">
|
<DialogTrigger asChild className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
@@ -116,114 +157,173 @@ 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 ">
|
<div className="grid gap-4">
|
||||||
{step === 1 && (
|
<div className="flex justify-between">
|
||||||
<>
|
<h2 className="text-lg font-semibold">Steps</h2>
|
||||||
{!haveAtleasOneProviderEnabled && (
|
<div className="flex items-center gap-2">
|
||||||
<AlertBlock type="warning">
|
<span className="text-sm text-muted-foreground">
|
||||||
<div className="flex flex-col w-full">
|
Step {stepper.current.index + 1} of {steps.length}
|
||||||
<span>AI features are not enabled</span>
|
</span>
|
||||||
<span>
|
<div />
|
||||||
To use AI-powered template generation, please{" "}
|
</div>
|
||||||
<Link
|
</div>
|
||||||
href="/dashboard/settings/ai"
|
<Scoped>
|
||||||
className="font-medium underline underline-offset-4"
|
<nav aria-label="Checkout Steps" className="group my-4">
|
||||||
>
|
<ol
|
||||||
enable AI in your settings
|
className="flex items-center justify-between gap-2"
|
||||||
</Link>
|
aria-orientation="horizontal"
|
||||||
.
|
>
|
||||||
</span>
|
{stepper.all.map((step, index, array) => (
|
||||||
</div>
|
<React.Fragment key={step.id}>
|
||||||
</AlertBlock>
|
<li className="flex items-center gap-4 flex-shrink-0">
|
||||||
)}
|
<Button
|
||||||
|
type="button"
|
||||||
{haveAtleasOneProviderEnabled &&
|
role="tab"
|
||||||
aiSettings &&
|
variant={
|
||||||
aiSettings?.length > 0 && (
|
index <= stepper.current.index ? "secondary" : "ghost"
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label
|
|
||||||
htmlFor="user-needs"
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
Select AI Provider
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={templateInfo.aiId}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setTemplateInfo((prev) => ({
|
|
||||||
...prev,
|
|
||||||
aiId: value,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
aria-current={
|
||||||
|
stepper.current.id === step.id ? "step" : undefined
|
||||||
|
}
|
||||||
|
aria-posinset={index + 1}
|
||||||
|
aria-setsize={steps.length}
|
||||||
|
aria-selected={stepper.current.id === step.id}
|
||||||
|
className="flex size-10 items-center justify-center rounded-full border-2 border-border"
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
{index + 1}
|
||||||
<SelectValue placeholder="Select an AI provider" />
|
</Button>
|
||||||
</SelectTrigger>
|
<span className="text-sm font-medium">{step.title}</span>
|
||||||
<SelectContent>
|
</li>
|
||||||
{aiSettings.map((ai) => (
|
{index < array.length - 1 && (
|
||||||
<SelectItem key={ai.aiId} value={ai.aiId}>
|
<Separator
|
||||||
{ai.name} ({ai.model})
|
className={`flex-1 ${
|
||||||
</SelectItem>
|
index < stepper.current.index
|
||||||
))}
|
? "bg-primary"
|
||||||
</SelectContent>
|
: "bg-muted"
|
||||||
</Select>
|
}`}
|
||||||
</div>
|
|
||||||
{templateInfo.aiId && (
|
|
||||||
<StepOne
|
|
||||||
nextStep={nextStep}
|
|
||||||
setTemplateInfo={setTemplateInfo}
|
|
||||||
templateInfo={templateInfo}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</React.Fragment>
|
||||||
)}
|
))}
|
||||||
</>
|
</ol>
|
||||||
)}
|
</nav>
|
||||||
{step === 2 && (
|
{stepper.switch({
|
||||||
<StepTwo
|
needs: () => (
|
||||||
nextStep={nextStep}
|
<>
|
||||||
prevStep={prevStep}
|
{!haveAtleasOneProviderEnabled && (
|
||||||
templateInfo={templateInfo}
|
<AlertBlock type="warning">
|
||||||
setTemplateInfo={setTemplateInfo}
|
<div className="flex flex-col w-full">
|
||||||
/>
|
<span>AI features are not enabled</span>
|
||||||
)}
|
<span>
|
||||||
{step === 3 && (
|
To use AI-powered template generation, please{" "}
|
||||||
<StepThree
|
<Link
|
||||||
prevStep={prevStep}
|
href="/dashboard/settings/ai"
|
||||||
templateInfo={templateInfo}
|
className="font-medium underline underline-offset-4"
|
||||||
nextStep={nextStep}
|
>
|
||||||
setTemplateInfo={setTemplateInfo}
|
enable AI in your settings
|
||||||
onSubmit={async () => {
|
</Link>
|
||||||
console.log("Submitting template:", templateInfo);
|
.
|
||||||
await mutateAsync({
|
</span>
|
||||||
projectId,
|
</div>
|
||||||
id: templateInfo.details?.id || "",
|
</AlertBlock>
|
||||||
name: templateInfo?.details?.name || "",
|
)}
|
||||||
description: templateInfo?.details?.shortDescription || "",
|
|
||||||
dockerCompose: templateInfo?.details?.dockerCompose || "",
|
{haveAtleasOneProviderEnabled &&
|
||||||
envVariables: (templateInfo?.details?.envVariables || [])
|
aiSettings &&
|
||||||
.map((env: any) => `${env.name}=${env.value}`)
|
aiSettings?.length > 0 && (
|
||||||
.join("\n"),
|
<div className="space-y-4">
|
||||||
domains: templateInfo?.details?.domains || [],
|
<div className="flex flex-col gap-2">
|
||||||
...(templateInfo.server?.serverId && {
|
<label
|
||||||
serverId: templateInfo.server?.serverId || "",
|
htmlFor="user-needs"
|
||||||
}),
|
className="text-sm font-medium"
|
||||||
})
|
>
|
||||||
.then(async () => {
|
Select AI Provider
|
||||||
toast.success("Compose Created");
|
</label>
|
||||||
setOpen(false);
|
<Select
|
||||||
await utils.project.one.invalidate({
|
value={templateInfo.aiId}
|
||||||
projectId,
|
onValueChange={(value) =>
|
||||||
});
|
setTemplateInfo((prev) => ({
|
||||||
})
|
...prev,
|
||||||
.catch(() => {
|
aiId: value,
|
||||||
toast.error("Error creating the compose");
|
}))
|
||||||
});
|
}
|
||||||
}}
|
>
|
||||||
/>
|
<SelectTrigger>
|
||||||
)}
|
<SelectValue placeholder="Select an AI provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{aiSettings.map((ai) => (
|
||||||
|
<SelectItem key={ai.aiId} value={ai.aiId}>
|
||||||
|
{ai.name} ({ai.model})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{templateInfo.aiId && (
|
||||||
|
<StepOne
|
||||||
|
setTemplateInfo={setTemplateInfo}
|
||||||
|
templateInfo={templateInfo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
variant: () => (
|
||||||
|
<StepTwo
|
||||||
|
templateInfo={templateInfo}
|
||||||
|
setTemplateInfo={setTemplateInfo}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
review: () => (
|
||||||
|
<StepThree
|
||||||
|
templateInfo={templateInfo}
|
||||||
|
setTemplateInfo={setTemplateInfo}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Scoped>
|
||||||
</div>
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<div className="flex items-center gap-2 w-full justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={stepper.prev}
|
||||||
|
disabled={stepper.isFirst}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={isDisabled()}
|
||||||
|
onClick={async () => {
|
||||||
|
if (stepper.current.id === "needs") {
|
||||||
|
setTemplateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
suggestions: [],
|
||||||
|
details: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepper.isLast) {
|
||||||
|
await onSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stepper.next();
|
||||||
|
// if (stepper.isLast) {
|
||||||
|
// // setIsOpen(false);
|
||||||
|
// // push("/dashboard/projects");
|
||||||
|
// } else {
|
||||||
|
// stepper.next();
|
||||||
|
// }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stepper.isLast ? "Create" : "Next"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,152 +1,152 @@
|
|||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
import { generatePassword } from "@/templates/utils";
|
import { generatePassword } from "@/templates/utils";
|
||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
import {
|
import {
|
||||||
apiCreateAi,
|
apiCreateAi,
|
||||||
apiUpdateAi,
|
apiUpdateAi,
|
||||||
deploySuggestionSchema,
|
deploySuggestionSchema,
|
||||||
} from "@dokploy/server/db/schema/ai";
|
} from "@dokploy/server/db/schema/ai";
|
||||||
import { createDomain } from "@dokploy/server/index";
|
import { createDomain } from "@dokploy/server/index";
|
||||||
import {
|
import {
|
||||||
deleteAiSettings,
|
deleteAiSettings,
|
||||||
getAiSettingById,
|
getAiSettingById,
|
||||||
getAiSettingsByAdminId,
|
getAiSettingsByAdminId,
|
||||||
saveAiSettings,
|
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";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
import {
|
import {
|
||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
} from "@dokploy/server/services/user";
|
} from "@dokploy/server/services/user";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const aiRouter = createTRPCRouter({
|
export const aiRouter = createTRPCRouter({
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const aiSetting = await getAiSettingById(input.aiId);
|
const aiSetting = await getAiSettingById(input.aiId);
|
||||||
if (aiSetting.adminId !== ctx.user.adminId) {
|
if (aiSetting.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You don't have access to this AI configuration",
|
message: "You don't have access to this AI configuration",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return aiSetting;
|
return aiSetting;
|
||||||
}),
|
}),
|
||||||
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
||||||
return await saveAiSettings(ctx.user.adminId, input);
|
return await saveAiSettings(ctx.user.adminId, input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateAi)
|
.input(apiUpdateAi)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
return await saveAiSettings(ctx.user.adminId, input);
|
return await saveAiSettings(ctx.user.adminId, input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getAll: adminProcedure.query(async ({ ctx }) => {
|
getAll: adminProcedure.query(async ({ ctx }) => {
|
||||||
return await getAiSettingsByAdminId(ctx.user.adminId);
|
return await getAiSettingsByAdminId(ctx.user.adminId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
get: protectedProcedure
|
get: protectedProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const aiSetting = await getAiSettingById(input.aiId);
|
const aiSetting = await getAiSettingById(input.aiId);
|
||||||
if (aiSetting.adminId !== ctx.user.authId) {
|
if (aiSetting.adminId !== ctx.user.authId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You don't have access to this AI configuration",
|
message: "You don't have access to this AI configuration",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return aiSetting;
|
return aiSetting;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const aiSetting = await getAiSettingById(input.aiId);
|
const aiSetting = await getAiSettingById(input.aiId);
|
||||||
if (aiSetting.adminId !== ctx.user.adminId) {
|
if (aiSetting.adminId !== ctx.user.adminId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You don't have access to this AI configuration",
|
message: "You don't have access to this AI configuration",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await deleteAiSettings(input.aiId);
|
return await deleteAiSettings(input.aiId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
suggest: protectedProcedure
|
suggest: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
aiId: z.string(),
|
aiId: z.string(),
|
||||||
input: z.string(),
|
input: z.string(),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
return await suggestVariants({
|
return await suggestVariants({
|
||||||
...input,
|
...input,
|
||||||
adminId: ctx.user.adminId,
|
adminId: ctx.user.adminId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(deploySuggestionSchema)
|
.input(deploySuggestionSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkServiceAccess(ctx.user.adminId, input.projectId, "create");
|
await checkServiceAccess(ctx.user.adminId, input.projectId, "create");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You need to use a server to create a compose",
|
message: "You need to use a server to create a compose",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
|
|
||||||
console.log(input);
|
console.log(input);
|
||||||
|
|
||||||
const compose = await createComposeByTemplate({
|
const compose = await createComposeByTemplate({
|
||||||
...input,
|
...input,
|
||||||
composeFile: input.dockerCompose,
|
composeFile: input.dockerCompose,
|
||||||
env: input.envVariables,
|
env: input.envVariables,
|
||||||
serverId: input.serverId,
|
serverId: input.serverId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
appName: `${projectName}-${generatePassword(6)}`,
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.domains && input.domains?.length > 0) {
|
if (input.domains && input.domains?.length > 0) {
|
||||||
for (const domain of input.domains) {
|
for (const domain of input.domains) {
|
||||||
await createDomain({
|
await createDomain({
|
||||||
...domain,
|
...domain,
|
||||||
domainType: "compose",
|
domainType: "compose",
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
composeId: compose.composeId,
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user