Merge branch 'canary' into feat/label-previews

This commit is contained in:
Mauricio Siu
2025-08-02 00:28:52 -06:00
27 changed files with 559 additions and 422 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}
@@ -151,7 +151,7 @@ export const HandleSecurity = ({
<FormItem> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<FormControl> <FormControl>
<Input placeholder="test" {...field} /> <Input placeholder="test" type="password" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -7,6 +7,9 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { LockKeyhole, Trash2 } from "lucide-react"; import { LockKeyhole, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -58,19 +61,18 @@ export const ShowSecurity = ({ applicationId }: Props) => {
<div className="flex flex-col gap-6 "> <div className="flex flex-col gap-6 ">
{data?.security.map((security) => ( {data?.security.map((security) => (
<div key={security.securityId}> <div key={security.securityId}>
<div className="flex w-full flex-col sm:flex-row justify-between sm:items-center gap-4 sm:gap-10 border rounded-lg p-4"> <div className="flex w-full flex-col md:flex-row justify-between md:items-center gap-4 md:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 flex-col gap-4 sm:gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 flex-col gap-4 md:gap-8">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-2">
<span className="font-medium">Username</span> <Label>Username</Label>
<span className="text-sm text-muted-foreground"> <Input disabled value={security.username} />
{security.username}
</span>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-2">
<span className="font-medium">Password</span> <Label>Password</Label>
<span className="text-sm text-muted-foreground"> <ToggleVisibilityInput
{security.password} value={security.password}
</span> disabled
/>
</div> </div>
</div> </div>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddTemplateSchema = z.object({ const AddTemplateSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -75,6 +75,8 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
const slug = slugify(projectName); const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation(); api.application.create.useMutation();
@@ -155,6 +157,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
{hasServers && (
<FormField <FormField
control={form.control} control={form.control}
name="serverId" name="serverId"
@@ -211,12 +214,27 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>App Name</FormLabel> <FormLabel className="flex items-center gap-2">
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="my-app" {...field} /> <Input placeholder="my-app" {...field} />
</FormControl> </FormControl>
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddComposeSchema = z.object({ const AddComposeSchema = z.object({
composeType: z.enum(["docker-compose", "stack"]).optional(), composeType: z.enum(["docker-compose", "stack"]).optional(),
@@ -78,6 +78,8 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation(); api.compose.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddCompose>({ const form = useForm<AddCompose>({
defaultValues: { defaultValues: {
name: "", name: "",
@@ -163,6 +165,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)} )}
/> />
</div> </div>
{hasServers && (
<FormField <FormField
control={form.control} control={form.control}
name="serverId" name="serverId"
@@ -219,6 +222,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"
@@ -1,3 +1,9 @@
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 { import {
MariadbIcon, MariadbIcon,
MongodbIcon, MongodbIcon,
@@ -37,14 +43,14 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
type DbType = typeof mySchema._type.type; type DbType = typeof mySchema._type.type;
@@ -163,6 +169,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const mariadbMutation = api.mariadb.create.useMutation(); const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddDatabase>({ const form = useForm<AddDatabase>({
defaultValues: { defaultValues: {
type: "postgres", type: "postgres",
@@ -374,6 +382,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
{hasServers && (
<FormField <FormField
control={form.control} control={form.control}
name="serverId" name="serverId"
@@ -407,12 +416,28 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>App Name</FormLabel> <FormLabel className="flex items-center gap-2">
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm
service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="my-app" {...field} /> <Input placeholder="my-app" {...field} />
</FormControl> </FormControl>
@@ -1,3 +1,18 @@
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { import {
@@ -54,21 +69,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url"; const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
@@ -137,6 +137,8 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
return matchesTags && matchesQuery; return matchesTags && matchesQuery;
}) || []; }) || [];
const hasServers = servers && servers.length > 0;
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
@@ -425,6 +427,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
project. project.
</AlertDialogDescription> </AlertDialogDescription>
{hasServers && (
<div> <div>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -441,9 +444,9 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
side="top" side="top"
> >
<span> <span>
If no server is selected, the application If no server is selected, the
will be deployed on the server where the application will be deployed on the
user is logged in. server where the user is logged in.
</span> </span>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -479,6 +482,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)}
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
@@ -25,6 +25,7 @@ const examples = [
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => { export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// 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 hasServers = servers && servers.length > 0;
const handleExampleClick = (example: string) => { const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example }); setTemplateInfo({ ...templateInfo, userInput: example });
@@ -47,6 +48,7 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/> />
</div> </div>
{hasServers && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="server-deploy"> <Label htmlFor="server-deploy">
Select the server where you want to deploy (optional) Select the server where you want to deploy (optional)
@@ -78,6 +80,7 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label>Examples:</Label> <Label>Examples:</Label>
@@ -199,7 +199,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Generating template suggestions based on your input... Generating template suggestions based on your input...
</p> </p>
<pre>{templateInfo.userInput}</pre> <pre className="whitespace-normal">{templateInfo.userInput}</pre>
</div> </div>
); );
} }
@@ -24,12 +24,14 @@ export const AddGithubProvider = () => {
const [isOrganization, setIsOrganization] = useState(false); const [isOrganization, setIsOrganization] = useState(false);
const [organizationName, setOrganization] = useState(""); const [organizationName, setOrganization] = useState("");
const randomString = () => Math.random().toString(36).slice(2, 8);
useEffect(() => { useEffect(() => {
const url = document.location.origin; const url = document.location.origin;
const manifest = JSON.stringify( const manifest = JSON.stringify(
{ {
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`, redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`, name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}-${randomString()}`,
url: origin, url: origin,
hook_attributes: { hook_attributes: {
url: `${url}/api/deploy/github`, url: `${url}/api/deploy/github`,
@@ -33,6 +33,7 @@ import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider"; import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider"; import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider"; import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
import { Badge } from "@/components/ui/badge";
export const ShowGitProviders = () => { export const ShowGitProviders = () => {
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery(); const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
@@ -158,7 +159,13 @@ export const ShowGitProviders = () => {
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
{!haveGithubRequirements && isGithub && ( {!haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1"> <div className="flex flex-row gap-1 items-center">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`} href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
className={buttonVariants({ className={buttonVariants({
@@ -185,7 +192,13 @@ export const ShowGitProviders = () => {
</div> </div>
)} )}
{!haveGitlabRequirements && isGitlab && ( {!haveGitlabRequirements && isGitlab && (
<div className="flex flex-col gap-1"> <div className="flex flex-row gap-1 items-center">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={getGitlabUrl( href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "", gitProvider.gitlab?.applicationId || "",
@@ -36,6 +36,7 @@ const profileSchema = z.object({
password: z.string().nullable(), password: z.string().nullable(),
currentPassword: z.string().nullable(), currentPassword: z.string().nullable(),
image: z.string().optional(), image: z.string().optional(),
name: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false), allowImpersonation: z.boolean().optional().default(false),
}); });
@@ -84,6 +85,7 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: "", currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false, allowImpersonation: data?.user?.allowImpersonation || false,
name: data?.user?.name || "",
}, },
resolver: zodResolver(profileSchema), resolver: zodResolver(profileSchema),
}); });
@@ -97,6 +99,7 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "", currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation, allowImpersonation: data?.user?.allowImpersonation,
name: data?.user?.name || "",
}, },
{ {
keepValues: true, keepValues: true,
@@ -119,6 +122,7 @@ export const ProfileForm = () => {
image: values.image, image: values.image,
currentPassword: values.currentPassword || undefined, currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation, allowImpersonation: values.allowImpersonation,
name: values.name || undefined,
}) })
.then(async () => { .then(async () => {
await refetch(); await refetch();
@@ -128,6 +132,7 @@ export const ProfileForm = () => {
password: "", password: "",
image: values.image, image: values.image,
currentPassword: "", currentPassword: "",
name: values.name || "",
}); });
}) })
.catch(() => { .catch(() => {
@@ -167,6 +172,19 @@ export const ProfileForm = () => {
className="grid gap-4" className="grid gap-4"
> >
<div className="space-y-4"> <div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
@@ -1,3 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -30,14 +38,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -218,7 +218,7 @@ export const HandleServers = ({ serverId }: Props) => {
</AlertBlock> </AlertBlock>
</div> </div>
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning" className="mt-4">
You cannot create more servers,{" "} You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary"> <Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan Please upgrade your plan
@@ -1,3 +1,9 @@
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -27,12 +33,6 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal"; import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
import { TerminalModal } from "../web-server/terminal-modal"; import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions"; import { ShowServerActions } from "./actions/show-server-actions";
@@ -115,24 +115,6 @@ export const ShowServers = () => {
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4 min-h-[25vh]"> <div className="flex flex-col gap-4 min-h-[25vh]">
{!canCreateMoreServers && (
<AlertBlock type="warning">
<div className="flex flex-row items-center gap-3 justify-center">
<span>
<div>
You cannot create more servers,{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Please upgrade your plan
</Link>
</div>
</span>
</div>
</AlertBlock>
)}
<Table> <Table>
<TableCaption> <TableCaption>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -22,12 +28,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -108,7 +108,7 @@ export const CreateServer = ({ stepper }: Props) => {
<Card className="bg-background flex flex-col gap-4"> <Card className="bg-background flex flex-col gap-4">
<div className="flex flex-col gap-2 pt-5 px-4"> <div className="flex flex-col gap-2 pt-5 px-4">
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning" className="mt-2">
You cannot create more servers,{" "} You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary"> <Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan Please upgrade your plan
@@ -1,18 +1,22 @@
import copy from "copy-to-clipboard";
import { CopyIcon, ExternalLinkIcon, Loader2 } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { ExternalLinkIcon, Loader2 } from "lucide-react";
import { CopyIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { toast } from "sonner";
export const CreateSSHKey = () => { export const CreateSSHKey = () => {
const { data, refetch } = api.sshKey.all.useQuery(); const { data, refetch } = api.sshKey.all.useQuery();
const generateMutation = api.sshKey.generate.useMutation(); const generateMutation = api.sshKey.generate.useMutation();
const { mutateAsync, isLoading } = api.sshKey.create.useMutation(); const { mutateAsync, isLoading } = api.sshKey.create.useMutation();
const hasCreatedKey = useRef(false); const hasCreatedKey = useRef(false);
const [selectedOption, setSelectedOption] = useState<"manual" | "provider">(
"manual",
);
const cloudSSHKey = data?.find( const cloudSSHKey = data?.find(
(sshKey) => sshKey.name === "dokploy-cloud-ssh-key", (sshKey) => sshKey.name === "dokploy-cloud-ssh-key",
@@ -60,27 +64,51 @@ export const CreateSSHKey = () => {
</div> </div>
) : ( ) : (
<> <>
<div className="flex flex-col gap-2 text-sm text-muted-foreground"> <div className="flex flex-col gap-4 text-sm text-muted-foreground">
<p className="text-primary text-base font-semibold"> <p className="text-primary text-base font-semibold">
You have two options to add SSH Keys to your server: Choose how to add SSH Keys to your server:
</p> </p>
<ul> {/* Radio button options */}
<li>1. Add The SSH Key to Server Manually</li> <div className="grid gap-2">
<RadioGroup
value={selectedOption}
onValueChange={(value) => {
setSelectedOption(value as "manual" | "provider");
}}
className="grid gap-3"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="manual" id="manual" />
<Label
htmlFor="manual"
className="text-primary font-medium cursor-pointer"
>
Add SSH Key to Server Manually
</Label>
</div>
<li> <div className="flex items-center space-x-2">
2. Add the public SSH Key when you create a server in your <RadioGroupItem value="provider" id="provider" />
preffered provider (Hostinger, Digital Ocean, Hetzner, etc){" "} <Label
</li> htmlFor="provider"
</ul> className="text-primary font-medium cursor-pointer"
>
Add SSH Key when creating server in your provider
</Label>
</div>
</RadioGroup>
</div>
{/* Content based on selected option */}
{selectedOption === "manual" && (
<div className="flex flex-col gap-2 w-full border rounded-lg p-4"> <div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary"> <span className="text-base font-semibold text-primary">
Option 1 Manual Setup Instructions
</span> </span>
<ul> <ul className="space-y-2">
<li className="items-center flex gap-1"> <li className="items-center flex gap-1">
1. Login to your server{" "} 1. Login to your server
</li> </li>
<li> <li>
2. When you are logged in run the following command 2. When you are logged in run the following command
@@ -107,14 +135,17 @@ export const CreateSSHKey = () => {
</div> </div>
</li> </li>
<li className="mt-1"> <li className="mt-1">
3. You're done, follow the next step to insert the details 3. You're done, follow the next step to insert the
of your server. details of your server.
</li> </li>
</ul> </ul>
</div> </div>
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4"> )}
{selectedOption === "provider" && (
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary"> <span className="text-base font-semibold text-primary">
Option 2 Provider Setup Instructions
</span> </span>
<div className="flex flex-col gap-4 w-full overflow-auto"> <div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex relative flex-col gap-2 overflow-y-auto"> <div className="flex relative flex-col gap-2 overflow-y-auto">
@@ -135,14 +166,20 @@ export const CreateSSHKey = () => {
</div> </div>
</div> </div>
</div> </div>
<p className="text-sm mt-2">
Use this public key when creating a server in your
preferred provider (Hostinger, Digital Ocean, Hetzner,
etc.)
</p>
<Link <Link
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements" href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
target="_blank" target="_blank"
className="text-primary flex flex-row gap-2" className="text-primary flex flex-row gap-2 mt-2"
> >
View Tutorial <ExternalLinkIcon className="size-4" /> View Tutorial <ExternalLinkIcon className="size-4" />
</Link> </Link>
</div> </div>
)}
</div> </div>
</> </>
)} )}
@@ -1,3 +1,4 @@
import { Layers, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -8,7 +9,6 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Layers, Loader2 } from "lucide-react";
import { type ApplicationList, columns } from "./columns"; import { type ApplicationList, columns } from "./columns";
import { DataTable } from "./data-table"; import { DataTable } from "./data-table";
@@ -20,10 +20,10 @@ export const ShowNodeApplications = ({ serverId }: Props) => {
const { data: NodeApps, isLoading: NodeAppsLoading } = const { data: NodeApps, isLoading: NodeAppsLoading } =
api.swarm.getNodeApps.useQuery({ serverId }); api.swarm.getNodeApps.useQuery({ serverId });
let applicationList = ""; let applicationList: string[] = [];
if (NodeApps && NodeApps.length > 0) { if (NodeApps && NodeApps.length > 0) {
applicationList = NodeApps.map((app) => app.Name).join(" "); applicationList = NodeApps.map((app) => app.Name);
} }
const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } =
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.24.4", "version": "v0.24.5",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
+7 -2
View File
@@ -1,10 +1,10 @@
import { import {
findServerById,
getApplicationInfo, getApplicationInfo,
getNodeApplications, getNodeApplications,
getNodeInfo, getNodeInfo,
getSwarmNodes, getSwarmNodes,
} from "@dokploy/server"; } from "@dokploy/server";
import { findServerById } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -55,7 +55,12 @@ export const swarmRouter = createTRPCRouter({
getAppInfos: protectedProcedure getAppInfos: protectedProcedure
.input( .input(
z.object({ z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."), appName: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid app name.")
.array()
.min(1),
serverId: z.string().optional(), serverId: z.string().optional(),
}), }),
) )
+2 -1
View File
@@ -19,7 +19,8 @@
}, },
"complexity": { "complexity": {
"noUselessCatch": "off", "noUselessCatch": "off",
"noBannedTypes": "off" "noBannedTypes": "off",
"noUselessFragments": "off"
}, },
"correctness": { "correctness": {
"useExhaustiveDependencies": "off", "useExhaustiveDependencies": "off",
+1
View File
@@ -323,6 +323,7 @@ export const apiUpdateWebServerMonitoring = z.object({
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
password: z.string().optional(), password: z.string().optional(),
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(),
metricsConfig: z metricsConfig: z
.object({ .object({
server: z.object({ server: z.object({
+6 -4
View File
@@ -237,6 +237,7 @@ export const deployApplication = async ({
} catch (error) { } catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({ await sendBuildErrorNotifications({
projectName: application.project.name, projectName: application.project.name,
applicationName: application.name, applicationName: application.name,
@@ -370,8 +371,9 @@ export const deployRemoteApplication = async ({
domains: application.domains, domains: application.domains,
}); });
} catch (error) { } catch (error) {
// @ts-ignore const errorMessage = error instanceof Error ? error.message : String(error);
const encodedContent = encodeBase64(error?.message);
const encodedContent = encodeBase64(errorMessage);
await execAsyncRemote( await execAsyncRemote(
application.serverId, application.serverId,
@@ -383,12 +385,12 @@ export const deployRemoteApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({ await sendBuildErrorNotifications({
projectName: application.project.name, projectName: application.project.name,
applicationName: application.name, applicationName: application.name,
applicationType: "application", applicationType: "application",
// @ts-ignore errorMessage: `Please check the logs for details: ${errorMessage}`,
errorMessage: error?.message || "Error building",
buildLink, buildLink,
organizationId: application.project.organizationId, organizationId: application.project.organizationId,
}); });
+14 -9
View File
@@ -1,8 +1,12 @@
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema"; import {
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; type apiCreateCompose,
buildAppName,
cleanAppName,
compose,
} from "@dokploy/server/db/schema";
import { import {
buildCompose, buildCompose,
getBuildComposeCommand, getBuildComposeCommand,
@@ -516,19 +520,20 @@ export const startCompose = async (composeId: string) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
try { try {
const { COMPOSE_PATH } = paths(!!compose.serverId); const { COMPOSE_PATH } = paths(!!compose.serverId);
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const path =
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
if (compose.composeType === "docker-compose") { if (compose.composeType === "docker-compose") {
if (compose.serverId) { if (compose.serverId) {
await execAsyncRemote( await execAsyncRemote(
compose.serverId, compose.serverId,
`cd ${join( `cd ${projectPath} && ${baseCommand}`,
COMPOSE_PATH,
compose.appName,
"code",
)} && docker compose -p ${compose.appName} up -d`,
); );
} else { } else {
await execAsync(`docker compose -p ${compose.appName} up -d`, { await execAsync(baseCommand, {
cwd: join(COMPOSE_PATH, compose.appName, "code"), cwd: projectPath,
}); });
} }
} }
+2 -2
View File
@@ -441,13 +441,13 @@ export const getNodeApplications = async (serverId?: string) => {
}; };
export const getApplicationInfo = async ( export const getApplicationInfo = async (
appName: string, appNames: string[],
serverId?: string, serverId?: string,
) => { ) => {
try { try {
let stdout = ""; let stdout = "";
let stderr = ""; let stderr = "";
const command = `docker service ps ${appName} --format '{{json .}}' --no-trunc`; const command = `docker service ps ${appNames.join(" ")} --format '{{json .}}' --no-trunc`;
if (serverId) { if (serverId) {
const result = await execAsyncRemote(serverId, command); const result = await execAsyncRemote(serverId, command);
@@ -65,6 +65,8 @@ export const sendBuildErrorNotifications = async ({
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim(); `${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"), title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245, color: 0xed4245,
@@ -101,7 +103,7 @@ export const sendBuildErrorNotifications = async ({
}, },
{ {
name: decorate("`⚠️`", "Error Message"), name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``, value: `\`\`\`${truncatedErrorMessage}\`\`\``,
}, },
{ {
name: decorate("`🧷`", "Build Link"), name: decorate("`🧷`", "Build Link"),
@@ -2,8 +2,7 @@ import path from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { findComposeById } from "@dokploy/server/services/compose"; import { findComposeById } from "@dokploy/server/services/compose";
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { normalizeS3Path } from "../backups/utils"; import { getS3Credentials, normalizeS3Path } from "../backups/utils";
import { getS3Credentials } from "../backups/utils";
export const backupVolume = async ( export const backupVolume = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>, volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
@@ -37,6 +36,9 @@ export const backupVolume = async (
echo "Starting upload to S3..." echo "Starting upload to S3..."
${rcloneCommand} ${rcloneCommand}
echo "Upload to S3 done ✅" echo "Upload to S3 done ✅"
echo "Cleaning up local backup file..."
rm "${volumeBackupPath}/${backupFileName}"
echo "Local backup file cleaned up ✅"
`; `;
if (!turnOff) { if (!turnOff) {