Compare commits

...

21 Commits

Author SHA1 Message Date
Mauricio Siu
17e9154887 Merge pull request #2257 from Dokploy/fix/send-build-error-on-remote-servers
Fix/send build error on remote servers
2025-07-28 01:52:57 -06:00
Mauricio Siu
2442494096 fix(application): simplify error message handling in deployment notifications 2025-07-28 01:51:21 -06:00
Mauricio Siu
bac2afb423 refactor(application): exclude appName from updateApplication data to streamline database updates 2025-07-28 01:50:58 -06:00
Mauricio Siu
4e9630e976 Merge pull request #2256 from Dokploy/feat/enhancements-cloud-version-ui
feat(dashboard): enhance application and database forms with tooltips…
2025-07-28 01:50:26 -06:00
Mauricio Siu
558f6aecae fix(application): improve error handling and notification messages during deployment 2025-07-28 01:48:33 -06:00
Mauricio Siu
c3e2b0d0f1 feat(dashboard): enhance application and database forms with tooltips for better user guidance 2025-07-28 01:12:43 -06:00
Mauricio Siu
11d584316a chore(package): bump version to v0.24.5 2025-07-28 00:57:44 -06:00
Mauricio Siu
f78dc555b2 Merge pull request #2244 from jhon2c/feat/improve-server-ux
feat(ux): Improve UX Based on Community Feedback
2025-07-27 23:21:24 -06:00
Mauricio Siu
5812b12a59 Merge pull request #2236 from masesisaac/canary
fix(dashboard): Update app security view to hide password
2025-07-27 23:16:07 -06:00
Mauricio Siu
7301d15e8f Merge pull request #2230 from amustapha/patch-1
fix: wrap user prompt in ai modal to prevent text stretch
2025-07-27 23:15:01 -06:00
Mauricio Siu
f79796a6c8 Merge pull request #2188 from Marukome0743/vscode
chore: add biome settings for vscode editor
2025-07-27 23:14:33 -06:00
Mauricio Siu
4122b37abd Merge pull request #2250 from Dokploy/feat/add-name-field-to-profile
feat(profile): add optional name field to user profile form and schema
2025-07-27 23:13:26 -06:00
Mauricio Siu
79e9593663 feat(profile): add optional name field to user profile form and schema 2025-07-27 23:13:06 -06:00
masesisaac
def3fa0030 fix(security): change password input type to 'password' 2025-07-28 04:58:43 +03:00
autofix-ci[bot]
d561068bcd [autofix.ci] apply automated fixes 2025-07-26 17:26:20 +00:00
Jhon
212c1b2d5f feat(dashboard): show "Action Required" badge for incomplete Git provider setup 2025-07-26 14:18:26 -03:00
Jhon
d3a54172b5 feat(ux): add conditional server selection functionality to application forms 2025-07-26 13:53:28 -03:00
masesisaac
cda33eb291 refactor(dashboard): reorder imports in show-security.tsx for consistency 2025-07-24 17:45:26 +03:00
masesisaac
c178234e53 fix(dashboard): hide basic auth password by default 2025-07-24 17:41:51 +03:00
Abdulhakeem Adetunji Mustapha
329db1fd1a fix: wrap user prompt in ai modal to prevent text stretch 2025-07-23 19:30:47 +01:00
Marukome0743
6efbf030a7 chore: add biome settings for vscode editor 2025-07-23 08:49:59 +09:00
21 changed files with 526 additions and 403 deletions

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome"]
}

8
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}

View File

@@ -151,7 +151,7 @@ export const HandleSecurity = ({
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="test" {...field} />
<Input placeholder="test" type="password" {...field} />
</FormControl>
<FormMessage />

View File

@@ -7,6 +7,9 @@ import {
CardHeader,
CardTitle,
} 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 { LockKeyhole, Trash2 } from "lucide-react";
import { toast } from "sonner";
@@ -58,19 +61,18 @@ export const ShowSecurity = ({ applicationId }: Props) => {
<div className="flex flex-col gap-6 ">
{data?.security.map((security) => (
<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="grid grid-cols-1 sm:grid-cols-2 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Username</span>
<span className="text-sm text-muted-foreground">
{security.username}
</span>
<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 md:grid-cols-2 flex-col gap-4 md:gap-8">
<div className="flex flex-col gap-2">
<Label>Username</Label>
<Input disabled value={security.username} />
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Password</span>
<span className="text-sm text-muted-foreground">
{security.password}
</span>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<ToggleVisibilityInput
value={security.password}
disabled
/>
</div>
</div>
<div className="flex flex-row gap-2">

View File

@@ -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 { Button } from "@/components/ui/button";
import {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
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({
name: z.string().min(1, {
@@ -75,6 +75,8 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation();
@@ -155,68 +157,84 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{hasServers && (
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<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>
<Input placeholder="my-app" {...field} />
</FormControl>

View File

@@ -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 { Button } from "@/components/ui/button";
import {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
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({
composeType: z.enum(["docker-compose", "stack"]).optional(),
@@ -78,6 +78,8 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddCompose>({
defaultValues: {
name: "",
@@ -163,62 +165,64 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)}
/>
</div>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{hasServers && (
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="appName"

View File

@@ -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 {
MariadbIcon,
MongodbIcon,
@@ -37,14 +43,14 @@ import {
} 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";
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;
@@ -163,6 +169,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddDatabase>({
defaultValues: {
type: "postgres",
@@ -374,45 +382,62 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a Server</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<SelectTrigger>
<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>
<FormMessage />
</FormItem>
)}
/>
{hasServers && (
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a Server</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<SelectTrigger>
<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>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<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>
<Input placeholder="my-app" {...field} />
</FormControl>

View File

@@ -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 { AlertBlock } from "@/components/shared/alert-block";
import {
@@ -54,21 +69,6 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
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";
@@ -137,6 +137,8 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
return matchesTags && matchesQuery;
}) || [];
const hasServers = servers && servers.length > 0;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full">
@@ -425,60 +427,62 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
project.
</AlertDialogDescription>
<div>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server{" "}
{!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application
will be deployed on the server where the
user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{hasServers && (
<div>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server{" "}
{!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the
application will be deployed on the
server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={(e) => {
setServerId(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
<Select
onValueChange={(e) => {
setServerId(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</span>
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>

View File

@@ -25,6 +25,7 @@ const examples = [
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example });
@@ -47,37 +48,39 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/>
</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>
{hasServers && (
<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">
<Label>Examples:</Label>

View File

@@ -199,7 +199,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<p className="text-muted-foreground">
Generating template suggestions based on your input...
</p>
<pre>{templateInfo.userInput}</pre>
<pre className="whitespace-normal">{templateInfo.userInput}</pre>
</div>
);
}

View File

@@ -33,6 +33,7 @@ import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
import { Badge } from "@/components/ui/badge";
export const ShowGitProviders = () => {
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
@@ -158,7 +159,13 @@ export const ShowGitProviders = () => {
<div className="flex flex-row gap-1">
{!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
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
className={buttonVariants({
@@ -185,7 +192,13 @@ export const ShowGitProviders = () => {
</div>
)}
{!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
href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "",

View File

@@ -36,6 +36,7 @@ const profileSchema = z.object({
password: z.string().nullable(),
currentPassword: z.string().nullable(),
image: z.string().optional(),
name: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false),
});
@@ -84,6 +85,7 @@ export const ProfileForm = () => {
image: data?.user?.image || "",
currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false,
name: data?.user?.name || "",
},
resolver: zodResolver(profileSchema),
});
@@ -97,6 +99,7 @@ export const ProfileForm = () => {
image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation,
name: data?.user?.name || "",
},
{
keepValues: true,
@@ -119,6 +122,7 @@ export const ProfileForm = () => {
image: values.image,
currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation,
name: values.name || undefined,
})
.then(async () => {
await refetch();
@@ -128,6 +132,7 @@ export const ProfileForm = () => {
password: "",
image: values.image,
currentPassword: "",
name: values.name || "",
});
})
.catch(() => {
@@ -167,6 +172,19 @@ export const ProfileForm = () => {
className="grid gap-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
control={form.control}
name="email"

View File

@@ -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 { Button } from "@/components/ui/button";
import {
@@ -30,14 +38,6 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
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({
name: z.string().min(1, {
@@ -218,7 +218,7 @@ export const HandleServers = ({ serverId }: Props) => {
</AlertBlock>
</div>
{!canCreateMoreServers && (
<AlertBlock type="warning">
<AlertBlock type="warning" className="mt-4">
You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan

View File

@@ -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 { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
@@ -27,12 +33,6 @@ import {
TableRow,
} from "@/components/ui/table";
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 { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions";
@@ -115,24 +115,6 @@ export const ShowServers = () => {
</div>
) : (
<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>
<TableCaption>
<div className="flex flex-col gap-4">

View File

@@ -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 { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
@@ -22,12 +28,6 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
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({
name: z.string().min(1, {
@@ -108,7 +108,7 @@ export const CreateServer = ({ stepper }: Props) => {
<Card className="bg-background flex flex-col gap-4">
<div className="flex flex-col gap-2 pt-5 px-4">
{!canCreateMoreServers && (
<AlertBlock type="warning">
<AlertBlock type="warning" className="mt-2">
You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan

View File

@@ -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 { 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 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 = () => {
const { data, refetch } = api.sshKey.all.useQuery();
const generateMutation = api.sshKey.generate.useMutation();
const { mutateAsync, isLoading } = api.sshKey.create.useMutation();
const hasCreatedKey = useRef(false);
const [selectedOption, setSelectedOption] = useState<"manual" | "provider">(
"manual",
);
const cloudSSHKey = data?.find(
(sshKey) => sshKey.name === "dokploy-cloud-ssh-key",
@@ -60,89 +64,122 @@ export const CreateSSHKey = () => {
</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">
You have two options to add SSH Keys to your server:
Choose how to add SSH Keys to your server:
</p>
<ul>
<li>1. Add The SSH Key to Server Manually</li>
{/* Radio button options */}
<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>
2. Add the public SSH Key when you create a server in your
preffered provider (Hostinger, Digital Ocean, Hetzner, etc){" "}
</li>
</ul>
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary">
Option 1
</span>
<ul>
<li className="items-center flex gap-1">
1. Login to your server{" "}
</li>
<li>
2. When you are logged in run the following command
<div className="flex relative flex-col gap-4 w-full mt-2">
<CodeEditor
lineWrapping
language="properties"
value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`}
readOnly
className="font-mono opacity-60"
/>
<button
type="button"
className="absolute right-2 top-2"
onClick={() => {
copy(
`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="size-4" />
</button>
</div>
</li>
<li className="mt-1">
3. You're done, follow the next step to insert the details
of your server.
</li>
</ul>
<div className="flex items-center space-x-2">
<RadioGroupItem value="provider" id="provider" />
<Label
htmlFor="provider"
className="text-primary font-medium cursor-pointer"
>
Add SSH Key when creating server in your provider
</Label>
</div>
</RadioGroup>
</div>
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4">
<span className="text-base font-semibold text-primary">
Option 2
</span>
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex relative flex-col gap-2 overflow-y-auto">
<div className="text-sm text-primary flex flex-row gap-2 items-center">
Copy Public Key
<button
type="button"
className="right-2 top-8"
onClick={() => {
copy(
cloudSSHKey?.publicKey || "Generate a SSH Key",
);
toast.success("SSH Copied to clipboard");
}}
>
<CopyIcon className="size-4 text-muted-foreground" />
</button>
{/* Content based on selected option */}
{selectedOption === "manual" && (
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary">
Manual Setup Instructions
</span>
<ul className="space-y-2">
<li className="items-center flex gap-1">
1. Login to your server
</li>
<li>
2. When you are logged in run the following command
<div className="flex relative flex-col gap-4 w-full mt-2">
<CodeEditor
lineWrapping
language="properties"
value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`}
readOnly
className="font-mono opacity-60"
/>
<button
type="button"
className="absolute right-2 top-2"
onClick={() => {
copy(
`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="size-4" />
</button>
</div>
</li>
<li className="mt-1">
3. You're done, follow the next step to insert the
details of your server.
</li>
</ul>
</div>
)}
{selectedOption === "provider" && (
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary">
Provider Setup Instructions
</span>
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex relative flex-col gap-2 overflow-y-auto">
<div className="text-sm text-primary flex flex-row gap-2 items-center">
Copy Public Key
<button
type="button"
className="right-2 top-8"
onClick={() => {
copy(
cloudSSHKey?.publicKey || "Generate a SSH Key",
);
toast.success("SSH Copied to clipboard");
}}
>
<CopyIcon className="size-4 text-muted-foreground" />
</button>
</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
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
target="_blank"
className="text-primary flex flex-row gap-2 mt-2"
>
View Tutorial <ExternalLinkIcon className="size-4" />
</Link>
</div>
<Link
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
target="_blank"
className="text-primary flex flex-row gap-2"
>
View Tutorial <ExternalLinkIcon className="size-4" />
</Link>
</div>
)}
</div>
</>
)}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.24.4",
"version": "v0.24.5",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -19,7 +19,8 @@
},
"complexity": {
"noUselessCatch": "off",
"noBannedTypes": "off"
"noBannedTypes": "off",
"noUselessFragments": "off"
},
"correctness": {
"useExhaustiveDependencies": "off",

View File

@@ -323,6 +323,7 @@ export const apiUpdateWebServerMonitoring = z.object({
export const apiUpdateUser = createSchema.partial().extend({
password: z.string().optional(),
currentPassword: z.string().optional(),
name: z.string().optional(),
metricsConfig: z
.object({
server: z.object({

View File

@@ -237,6 +237,7 @@ export const deployApplication = async ({
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.project.name,
applicationName: application.name,
@@ -370,8 +371,9 @@ export const deployRemoteApplication = async ({
domains: application.domains,
});
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
const errorMessage = error instanceof Error ? error.message : String(error);
const encodedContent = encodeBase64(errorMessage);
await execAsyncRemote(
application.serverId,
@@ -383,12 +385,12 @@ export const deployRemoteApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.project.name,
applicationName: application.name,
applicationType: "application",
// @ts-ignore
errorMessage: error?.message || "Error building",
errorMessage: `Please check the logs for details: ${errorMessage}`,
buildLink,
organizationId: application.project.organizationId,
});

View File

@@ -65,6 +65,8 @@ export const sendBuildErrorNotifications = async ({
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245,
@@ -101,7 +103,7 @@ export const sendBuildErrorNotifications = async ({
},
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``,
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
},
{
name: decorate("`🧷`", "Build Link"),