import { zodResolver } from "@hookform/resolvers/zod"; import { AlertTriangle, Database, HelpCircle } from "lucide-react"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { MariadbIcon, MongodbIcon, MysqlIcon, PostgresqlIcon, RedisIcon, } from "@/components/icons/data-tools-icons"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { slugify } from "@/lib/slug"; import { api } from "@/utils/api"; type DbType = typeof mySchema._type.type; const dockerImageDefaultPlaceholder: Record = { mongo: "mongo:7", mariadb: "mariadb:11", mysql: "mysql:8", postgres: "postgres:15", redis: "redis:7", }; const databasesUserDefaultPlaceholder: Record< Exclude, string > = { mongo: "mongo", mariadb: "mariadb", mysql: "mysql", postgres: "postgres", }; const baseDatabaseSchema = z.object({ name: z.string().min(1, "Name required"), appName: z .string() .min(1, { message: "App name is required", }) .regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, { message: "App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'", }), databasePassword: z .string() .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { message: "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", }), dockerImage: z.string(), description: z.string().nullable(), serverId: z.string().nullable(), }); const mySchema = z.discriminatedUnion("type", [ z .object({ type: z.literal("postgres"), databaseName: z.string().default("postgres"), databaseUser: z.string().default("postgres"), }) .merge(baseDatabaseSchema), z .object({ type: z.literal("mongo"), databaseUser: z.string().default("mongo"), replicaSets: z.boolean().default(false), }) .merge(baseDatabaseSchema), z .object({ type: z.literal("redis"), }) .merge(baseDatabaseSchema), z .object({ type: z.literal("mysql"), databaseRootPassword: z .string() .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { message: "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", }) .optional(), databaseUser: z.string().default("mysql"), databaseName: z.string().default("mysql"), }) .merge(baseDatabaseSchema), z .object({ type: z.literal("mariadb"), dockerImage: z.string().default("mariadb:4"), databaseRootPassword: z .string() .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { message: "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility", }) .optional(), databaseUser: z.string().default("mariadb"), databaseName: z.string().default("mariadb"), }) .merge(baseDatabaseSchema), ]); const databasesMap = { postgres: { icon: , label: "PostgreSQL", }, mongo: { icon: , label: "MongoDB", }, mariadb: { icon: , label: "MariaDB", }, mysql: { icon: , label: "MySQL", }, redis: { icon: , label: "Redis", }, }; type AddDatabase = z.infer; interface Props { environmentId: string; projectName?: string; } export const AddDatabase = ({ environmentId, projectName }: Props) => { const utils = api.useUtils(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery(); const postgresMutation = api.postgres.create.useMutation(); const mongoMutation = api.mongo.create.useMutation(); const redisMutation = api.redis.create.useMutation(); const mariadbMutation = api.mariadb.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation(); // Get environment data to extract projectId const { data: environment } = api.environment.one.useQuery({ environmentId }); const hasServers = servers && servers.length > 0; // Show dropdown logic based on cloud environment // Cloud: show only if there are remote servers (no Dokploy option) // Self-hosted: show only if there are remote servers (Dokploy is default, hide if no remote servers) const shouldShowServerDropdown = hasServers; const form = useForm({ defaultValues: { type: "postgres", dockerImage: "", name: "", appName: `${slug}-`, databasePassword: "", description: "", databaseName: "", databaseUser: "", serverId: null, }, resolver: zodResolver(mySchema), }); const type = form.watch("type"); const activeMutation = { postgres: postgresMutation, mongo: mongoMutation, redis: redisMutation, mariadb: mariadbMutation, mysql: mysqlMutation, }; const onSubmit = async (data: AddDatabase) => { const defaultDockerImage = data.dockerImage || dockerImageDefaultPlaceholder[data.type]; let promise: Promise | null = null; const commonParams = { name: data.name, appName: data.appName, dockerImage: defaultDockerImage, serverId: data.serverId === "dokploy" ? undefined : data.serverId, environmentId, description: data.description, }; if (data.type === "postgres") { promise = postgresMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, databaseName: data.databaseName || "postgres", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mongo") { promise = mongoMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], serverId: data.serverId === "dokploy" ? null : data.serverId, replicaSets: data.replicaSets, }); } else if (data.type === "redis") { promise = redisMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mariadb") { promise = mariadbMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, databaseRootPassword: data.databaseRootPassword || "", databaseName: data.databaseName || "mariadb", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], serverId: data.serverId === "dokploy" ? null : data.serverId, }); } else if (data.type === "mysql") { promise = mysqlMutation.mutateAsync({ ...commonParams, databasePassword: data.databasePassword, databaseName: data.databaseName || "mysql", databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], serverId: data.serverId === "dokploy" ? null : data.serverId, databaseRootPassword: data.databaseRootPassword || "", }); } if (promise) { await promise .then(async () => { toast.success("Database Created"); form.reset({ type: "postgres", dockerImage: "", name: "", appName: `${projectName}-`, databasePassword: "", description: "", databaseName: "", databaseUser: "", }); setVisible(false); // Invalidate the project query to refresh the environment data await utils.environment.one.invalidate({ environmentId, }); }) .catch(() => { toast.error("Error creating a database"); }); } }; return ( e.preventDefault()} > Database Databases
( Select a database {Object.entries(databasesMap).map(([key, value]) => (
))}
{activeMutation[field.value].isError && (
{activeMutation[field.value].error?.message}
)}
)} />
Fill the next fields.
( Name { const val = e.target.value || ""; const serviceName = slugify(val.trim()); form.setValue("appName", `${slug}-${serviceName}`); field.onChange(val); }} /> )} /> {shouldShowServerDropdown && ( ( Select a Server )} /> )} ( App Name

This will be the name of the Docker Swarm service

)} /> ( Description