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
.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> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<FormControl> <FormControl>
<Input placeholder="test" {...field} /> <Input placeholder="test" type="password" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -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">

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 { 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,68 +157,84 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField {hasServers && (
control={form.control} <FormField
name="serverId" control={form.control}
render={({ field }) => ( name="serverId"
<FormItem> render={({ field }) => (
<TooltipProvider delayDuration={0}> <FormItem>
<Tooltip> <TooltipProvider delayDuration={0}>
<TooltipTrigger asChild> <Tooltip>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center"> <TooltipTrigger asChild>
Select a Server {!isCloud ? "(Optional)" : ""} <FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" /> Select a Server {!isCloud ? "(Optional)" : ""}
</FormLabel> <HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger> </FormLabel>
<TooltipContent </TooltipTrigger>
className="z-[999] w-[300px]" <TooltipContent
align="start" className="z-[999] w-[300px]"
side="top" align="start"
> side="top"
<span> >
If no server is selected, the application will be <span>
deployed on the server where the user is logged in. If no server is selected, the application will be
</span> deployed on the server where the user is logged in.
</TooltipContent> </span>
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</TooltipProvider>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</span> </SelectItem>
</SelectItem> ))}
))} <SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>Servers ({servers?.length})</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> <FormMessage />
<FormMessage /> </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>

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 { 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,62 +165,64 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)} )}
/> />
</div> </div>
<FormField {hasServers && (
control={form.control} <FormField
name="serverId" control={form.control}
render={({ field }) => ( name="serverId"
<FormItem> render={({ field }) => (
<TooltipProvider delayDuration={0}> <FormItem>
<Tooltip> <TooltipProvider delayDuration={0}>
<TooltipTrigger asChild> <Tooltip>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center"> <TooltipTrigger asChild>
Select a Server {!isCloud ? "(Optional)" : ""} <FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" /> Select a Server {!isCloud ? "(Optional)" : ""}
</FormLabel> <HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger> </FormLabel>
<TooltipContent </TooltipTrigger>
className="z-[999] w-[300px]" <TooltipContent
align="start" className="z-[999] w-[300px]"
side="top" align="start"
> side="top"
<span> >
If no server is selected, the application will be <span>
deployed on the server where the user is logged in. If no server is selected, the application will be
</span> deployed on the server where the user is logged in.
</TooltipContent> </span>
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</TooltipProvider>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</span> </SelectItem>
</SelectItem> ))}
))} <SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>Servers ({servers?.length})</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> )}
)} />
/> )}
<FormField <FormField
control={form.control} control={form.control}
name="appName" 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 { 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,45 +382,62 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField {hasServers && (
control={form.control} <FormField
name="serverId" control={form.control}
render={({ field }) => ( name="serverId"
<FormItem> render={({ field }) => (
<FormLabel>Select a Server</FormLabel> <FormItem>
<Select <FormLabel>Select a Server</FormLabel>
onValueChange={field.onChange} <Select
defaultValue={field.value || ""} onValueChange={field.onChange}
> defaultValue={field.value || ""}
<SelectTrigger> >
<SelectValue placeholder="Select a Server" /> <SelectTrigger>
</SelectTrigger> <SelectValue placeholder="Select a Server" />
<SelectContent> </SelectTrigger>
<SelectGroup> <SelectContent>
{servers?.map((server) => ( <SelectGroup>
<SelectItem {servers?.map((server) => (
key={server.serverId} <SelectItem
value={server.serverId} key={server.serverId}
> value={server.serverId}
{server.name} >
</SelectItem> {server.name}
))} </SelectItem>
<SelectLabel> ))}
Servers ({servers?.length}) <SelectLabel>
</SelectLabel> Servers ({servers?.length})
</SelectGroup> </SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
<FormMessage /> </Select>
</FormItem> <FormMessage />
)} </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>

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

View File

@@ -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,37 +48,39 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/> />
</div> </div>
<div className="space-y-2"> {hasServers && (
<Label htmlFor="server-deploy"> <div className="space-y-2">
Select the server where you want to deploy (optional) <Label htmlFor="server-deploy">
</Label> Select the server where you want to deploy (optional)
<Select </Label>
value={templateInfo.server?.serverId} <Select
onValueChange={(value) => { value={templateInfo.server?.serverId}
const server = servers?.find((s) => s.serverId === value); onValueChange={(value) => {
if (server) { const server = servers?.find((s) => s.serverId === value);
setTemplateInfo({ if (server) {
...templateInfo, setTemplateInfo({
server: server, ...templateInfo,
}); server: server,
} });
}} }
> }}
<SelectTrigger className="w-full"> >
<SelectValue placeholder="Select a server" /> <SelectTrigger className="w-full">
</SelectTrigger> <SelectValue placeholder="Select a server" />
<SelectContent> </SelectTrigger>
<SelectGroup> <SelectContent>
{servers?.map((server) => ( <SelectGroup>
<SelectItem key={server.serverId} value={server.serverId}> {servers?.map((server) => (
{server.name} <SelectItem key={server.serverId} value={server.serverId}>
</SelectItem> {server.name}
))} </SelectItem>
<SelectLabel>Servers ({servers?.length})</SelectLabel> ))}
</SelectGroup> <SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
</div> </Select>
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label>Examples:</Label> <Label>Examples:</Label>

View File

@@ -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>
); );
} }

View File

@@ -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`,

View File

@@ -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 || "",

View File

@@ -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"

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 { 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

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 { 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">

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 { 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

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 { 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,89 +64,122 @@ 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"
>
<div className="flex flex-col gap-2 w-full border rounded-lg p-4"> Add SSH Key when creating server in your provider
<span className="text-base font-semibold text-primary"> </Label>
Option 1 </div>
</span> </RadioGroup>
<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> </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"> {/* Content based on selected option */}
Option 2 {selectedOption === "manual" && (
</span> <div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<div className="flex flex-col gap-4 w-full overflow-auto"> <span className="text-base font-semibold text-primary">
<div className="flex relative flex-col gap-2 overflow-y-auto"> Manual Setup Instructions
<div className="text-sm text-primary flex flex-row gap-2 items-center"> </span>
Copy Public Key <ul className="space-y-2">
<button <li className="items-center flex gap-1">
type="button" 1. Login to your server
className="right-2 top-8" </li>
onClick={() => { <li>
copy( 2. When you are logged in run the following command
cloudSSHKey?.publicKey || "Generate a SSH Key", <div className="flex relative flex-col gap-4 w-full mt-2">
); <CodeEditor
toast.success("SSH Copied to clipboard"); lineWrapping
}} language="properties"
> value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`}
<CopyIcon className="size-4 text-muted-foreground" /> readOnly
</button> 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>
</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> </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> </div>
</> </>
)} )}

View File

@@ -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 } =

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",

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(),
}), }),
) )

View File

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

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({

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,
}); });

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,
}); });
} }
} }

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);

View File

@@ -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"),

View File

@@ -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) {