Merge branch 'canary' into 319-ability-to-roll-back-service-depoyments

This commit is contained in:
Mauricio Siu
2025-06-01 19:44:02 -06:00
109 changed files with 9421 additions and 4008 deletions

View File

@@ -12,7 +12,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.9.0
node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
@@ -26,7 +26,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.9.0
node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
@@ -39,7 +39,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.9.0
node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build

2
.nvmrc
View File

@@ -1 +1 @@
20.9.0
20.16.0

View File

@@ -52,7 +52,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory.
```bash
git clone https://github.com/dokploy/dokploy.git

View File

@@ -29,7 +29,7 @@ WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.35.0
ARG NIXPACKS_VERSION=1.39.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \
&& ./install.sh \
@@ -63,4 +63,4 @@ RUN curl -sSL https://railpack.com/install.sh | bash
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
CMD [ "pnpm", "start" ]
CMD [ "pnpm", "start" ]

View File

@@ -148,19 +148,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
</a>
<!-- ## Supported OS
- Ubuntu 24.04 LTS
- Ubuntu 23.10
- Ubuntu 22.04 LTS
- Ubuntu 20.04 LTS
- Ubuntu 18.04 LTS
- Debian 12
- Debian 11
- Fedora 40
- Centos 9
- Centos 8 -->
## Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.

View File

@@ -1 +1 @@
20.9.0
20.16.0

View File

@@ -1,26 +0,0 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# Build only the dokploy app
RUN pnpm run dokploy:build
# Deploy only the dokploy app
RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
FROM base AS dokploy
COPY --from=build /prod/dokploy /prod/dokploy
WORKDIR /prod/dokploy
EXPOSE 3000
CMD [ "pnpm", "start" ]

View File

@@ -105,6 +105,7 @@ const baseApp: ApplicationNested = {
ports: [],
projectId: "",
publishDirectory: null,
isStaticSpa: null,
redirects: [],
refreshToken: "",
registry: null,
@@ -149,67 +150,68 @@ describe("unzipDrop using real zip files", () => {
} finally {
}
});
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
baseApp.appName = "folderwithfile";
// const appName = "folderwithfile";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
});
it("should correctly extract a zip with multiple root folders", async () => {
baseApp.appName = "two-folders";
// const appName = "two-folders";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "folder2")).toBe(true);
});
it("should correctly extract a zip with a single root with a file", async () => {
baseApp.appName = "nested";
// const appName = "nested";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "folder2")).toBe(true);
expect(files.some((f) => f.name === "folder3")).toBe(true);
});
it("should correctly extract a zip with a single root with a folder", async () => {
baseApp.appName = "folder-with-sibling-file";
// const appName = "folder-with-sibling-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "test.txt")).toBe(true);
});
});
// it("should correctly extract a zip with a single root folder and a subfolder", async () => {
// baseApp.appName = "folderwithfile";
// // const appName = "folderwithfile";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
// });
// it("should correctly extract a zip with multiple root folders", async () => {
// baseApp.appName = "two-folders";
// // const appName = "two-folders";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "folder2")).toBe(true);
// });
// it("should correctly extract a zip with a single root with a file", async () => {
// baseApp.appName = "nested";
// // const appName = "nested";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/nested.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "folder2")).toBe(true);
// expect(files.some((f) => f.name === "folder3")).toBe(true);
// });
// it("should correctly extract a zip with a single root with a folder", async () => {
// baseApp.appName = "folder-with-sibling-file";
// // const appName = "folder-with-sibling-file";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "test.txt")).toBe(true);
// });
// });

View File

@@ -85,6 +85,7 @@ const baseApp: ApplicationNested = {
ports: [],
projectId: "",
publishDirectory: null,
isStaticSpa: null,
redirects: [],
refreshToken: "",
registry: null,

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
import { describe, expect, test } from "vitest";
describe("normalizeS3Path", () => {
test("should handle empty and whitespace-only prefix", () => {

View File

@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
{templateInfo.template.envs.map((env, index) => (
<div
key={index}
className="rounded-lg border bg-card p-2 font-mono text-sm"
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
>
{env}
</div>
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
<DialogDescription>Mount File Content</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[25vh] pr-4">
<ScrollArea className="h-[45vh] pr-4">
<CodeEditor
language="yaml"
value={selectedMount?.content || ""}

View File

@@ -2,6 +2,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import {
Form,
FormControl,
@@ -63,10 +64,11 @@ const mySchema = z.discriminatedUnion("buildType", [
publishDirectory: z.string().optional(),
}),
z.object({
buildType: z.literal(BuildType.static),
buildType: z.literal(BuildType.railpack),
}),
z.object({
buildType: z.literal(BuildType.railpack),
buildType: z.literal(BuildType.static),
isStaticSpa: z.boolean().default(false),
}),
]);
@@ -83,6 +85,7 @@ interface ApplicationData {
dockerBuildStage?: string | null;
herokuVersion?: string | null;
publishDirectory?: string | null;
isStaticSpa?: boolean | null;
}
function isValidBuildType(value: string): value is BuildType {
@@ -115,16 +118,18 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.static:
return {
buildType: BuildType.static,
isStaticSpa: data.isStaticSpa ?? false,
};
case BuildType.railpack:
return {
buildType: BuildType.railpack,
};
default:
default: {
const buildType = data.buildType as BuildType;
return {
buildType,
} as AddTemplate;
}
}
};
@@ -174,6 +179,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === BuildType.heroku_buildpacks
? data.herokuVersion
: null,
isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null,
})
.then(async () => {
toast.success("Build type saved");
@@ -364,6 +371,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)}
/>
)}
{buildType === BuildType.static && (
<FormField
control={form.control}
name="isStaticSpa"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-x-2 p-2">
<Checkbox
id="checkboxIsStaticSpa"
value={String(field.value)}
checked={field.value}
onCheckedChange={field.onChange}
/>
<FormLabel htmlFor="checkboxIsStaticSpa">
Single Page Application (SPA)
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save

View File

@@ -1,5 +1,6 @@
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -9,12 +10,11 @@ import {
CardTitle,
} from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon, Clock, Loader2 } from "lucide-react";
import { Clock, Loader2, RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
import { Badge } from "@/components/ui/badge";
interface Props {
id: string;
@@ -86,7 +86,7 @@ export const ShowDeployments = ({
<span>Webhook URL: </span>
<div className="flex flex-row items-center gap-2">
<span className="break-all text-muted-foreground">
{`${url}/api/deploy/${refreshToken}`}
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
</span>
{(type === "application" || type === "compose") && (
<RefreshToken id={id} type={type} />

View File

@@ -1,3 +1,5 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -6,8 +8,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { Copy, HelpCircle, Server } from "lucide-react";
import { toast } from "sonner";

View File

@@ -1,4 +1,5 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -7,6 +8,12 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import {
CheckCircle2,
@@ -21,17 +28,10 @@ import {
XCircle,
} from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { AddDomain } from "./handle-domain";
import { useState } from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { toast } from "sonner";
import { DnsHelperModal } from "./dns-helper-modal";
import { Badge } from "@/components/ui/badge";
import { AddDomain } from "./handle-domain";
export type ValidationState = {
isLoading: boolean;
@@ -186,30 +186,19 @@ export const ShowDomains = ({ id, type }: Props) => {
return (
<Card
key={item.domainId}
className="relative overflow-hidden w-full border bg-card transition-all hover:shadow-md bg-transparent h-fit"
className="relative overflow-hidden w-full border transition-all hover:shadow-md bg-transparent h-fit"
>
<CardContent className="p-6">
<div className="flex flex-col gap-4">
{/* Service & Domain Info */}
<div className="flex items-start justify-between">
<div className="flex flex-col gap-2">
{item.serviceName && (
<Badge variant="outline" className="w-fit">
<Server className="size-3 mr-1" />
{item.serviceName}
</Badge>
)}
<Link
className="flex items-center gap-2 text-base font-medium hover:underline"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
{item.host}
<ExternalLink className="size-4" />
</Link>
</div>
<div className="flex gap-2">
<div className="flex items-center justify-between flex-wrap gap-y-2">
{item.serviceName && (
<Badge variant="outline" className="w-fit">
<Server className="size-3 mr-1" />
{item.serviceName}
</Badge>
)}
<div className="flex gap-2 flex-wrap">
{!item.host.includes("traefik.me") && (
<DnsHelperModal
domain={{
@@ -266,6 +255,16 @@ export const ShowDomains = ({ id, type }: Props) => {
</DialogAction>
</div>
</div>
<div className="w-full break-all">
<Link
className="flex items-center gap-2 text-base font-medium hover:underline"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
{item.host}
<ExternalLink className="size-4 min-w-4" />
</Link>
</div>
{/* Domain Details */}
<div className="flex flex-wrap gap-3">

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
@@ -435,7 +435,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -454,7 +454,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
registryURL: data.registryUrl || "",
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({

View File

@@ -17,13 +17,13 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
@@ -262,7 +262,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -281,7 +281,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
@@ -470,7 +470,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GithubProvider) => {
await mutateAsync({
@@ -474,7 +474,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
@@ -452,7 +452,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -24,9 +24,9 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { AddPreviewDomain } from "./add-preview-domain";
import { ShowPreviewSettings } from "./show-preview-settings";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
interface Props {
applicationId: string;

View File

@@ -1,40 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Info,
PlusCircle,
PenBoxIcon,
RefreshCw,
DatabaseZap,
} from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
@@ -42,10 +8,44 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import {
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { CacheType } from "../domains/handle-domain";
export const commonCronExpressions = [

View File

@@ -1,14 +1,6 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { HandleSchedules } from "./handle-schedules";
import {
Clock,
Play,
Terminal,
Trash2,
ClipboardList,
Loader2,
} from "lucide-react";
import {
Card,
CardContent,
@@ -16,16 +8,24 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { api } from "@/utils/api";
import {
ClipboardList,
Clock,
Loader2,
Play,
Terminal,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleSchedules } from "./handle-schedules";
interface Props {
id: string;

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
@@ -437,7 +437,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -456,7 +456,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -263,7 +263,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -282,7 +282,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
@@ -437,7 +437,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GithubProvider) => {
await mutateAsync({
@@ -474,7 +474,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -496,7 +496,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
@@ -453,7 +453,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -472,7 +472,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -71,8 +71,8 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
isolatedDeployment: formData?.isolatedDeployment || false,
})
.then(async (_data) => {
randomizeCompose();
refetch();
await randomizeCompose();
await refetch();
toast.success("Compose updated");
})
.catch(() => {
@@ -84,15 +84,10 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
await mutateAsync({
composeId,
suffix: data?.appName || "",
})
.then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
toast.success("Compose Isolated");
})
.catch(() => {
toast.error("Error isolating the compose");
});
}).then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
});
};
return (

View File

@@ -77,8 +77,8 @@ export const RandomizeCompose = ({ composeId }: Props) => {
randomize: formData?.randomize || false,
})
.then(async (_data) => {
randomizeCompose();
refetch();
await randomizeCompose();
await refetch();
toast.success("Compose updated");
})
.catch(() => {
@@ -90,15 +90,10 @@ export const RandomizeCompose = ({ composeId }: Props) => {
await mutateAsync({
composeId,
suffix,
})
.then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
toast.success("Compose randomized");
})
.catch(() => {
toast.error("Error randomizing the compose");
});
}).then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
});
};
return (

View File

@@ -10,7 +10,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { Puzzle, RefreshCw } from "lucide-react";
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
@@ -66,36 +66,50 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
</AlertBlock>
{isLoading ? (
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md">
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
</div>
) : compose?.length === 5 ? (
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-[25rem]">
<Puzzle className="h-8 w-8 text-muted-foreground mb-2" />
<span className="text-muted-foreground">
No converted compose data available.
</span>
</div>
) : (
<>
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"
isLoading={isLoading}
onClick={() => {
mutateAsync({ composeId })
.then(() => {
refetch();
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error fetching source type", {
description: err.message,
});
});
}}
>
Refresh <RefreshCw className="ml-2 h-4 w-4" />
</Button>
</div>
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"
isLoading={isLoading}
onClick={() => {
mutateAsync({ composeId })
.then(() => {
refetch();
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error fetching source type", {
description: err.message,
});
});
}}
>
Refresh <RefreshCw className="ml-2 h-4 w-4" />
</Button>
</div>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</>
)}
</DialogContent>
</Dialog>
);

View File

@@ -39,6 +39,12 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -48,9 +54,9 @@ import {
CheckIcon,
ChevronsUpDown,
Copy,
RotateCcw,
RefreshCw,
DatabaseZap,
RefreshCw,
RotateCcw,
} from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -58,12 +64,6 @@ import { toast } from "sonner";
import { z } from "zod";
import type { ServiceType } from "../../application/advanced/show-resources";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type DatabaseType =
| Exclude<ServiceType, "application" | "redis">

View File

@@ -1,3 +1,10 @@
import {
MariadbIcon,
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
@@ -13,6 +20,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import {
ClipboardList,
@@ -25,17 +33,9 @@ import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources";
import { RestoreBackup } from "./restore-backup";
import { HandleBackup } from "./handle-backup";
import { cn } from "@/lib/utils";
import {
MariadbIcon,
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
import { HandleBackup } from "./handle-backup";
import { RestoreBackup } from "./restore-backup";
interface Props {
id: string;

View File

@@ -1,24 +1,9 @@
"use client";
import { authClient } from "@/lib/auth-client";
import { useEffect, useState } from "react";
import { Logo } from "@/components/shared/logo";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
CheckIcon,
ChevronsUpDown,
Settings2,
UserIcon,
XIcon,
Shield,
Calendar,
Key,
Copy,
Fingerprint,
Building2,
CreditCard,
Server,
} from "lucide-react";
import { toast } from "sonner";
import {
Command,
CommandEmpty,
@@ -32,19 +17,34 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Logo } from "@/components/shared/logo";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { format } from "date-fns";
import copy from "copy-to-clipboard";
import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { format } from "date-fns";
import {
Building2,
Calendar,
CheckIcon,
ChevronsUpDown,
Copy,
CreditCard,
Fingerprint,
Key,
Server,
Settings2,
Shield,
UserIcon,
XIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
type User = typeof authClient.$Infer.Session.user;

View File

@@ -10,6 +10,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api";
import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router";
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
const utils = api.useUtils();
const router = useRouter();
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({
onSuccess: async (newProject) => {
await utils.project.all.invalidate();
toast.success("Project duplicated successfully");
toast.success(
duplicateType === "new-project"
? "Project duplicated successfully"
: "Services duplicated successfully",
);
setOpen(false);
router.push(`/dashboard/project/${newProject.projectId}`);
if (duplicateType === "new-project") {
router.push(`/dashboard/project/${newProject.projectId}`);
}
},
onError: (error) => {
toast.error(error.message);
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
});
const handleDuplicate = async () => {
if (!name) {
if (duplicateType === "new-project" && !name) {
toast.error("Project name is required");
return;
}
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
id: service.id,
type: service.type,
})),
duplicateInSameProject: duplicateType === "same-project",
});
};
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
// Reset form when closing
setName("");
setDescription("");
setDuplicateType("new-project");
}
}}
>
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle>
<DialogTitle>Duplicate Services</DialogTitle>
<DialogDescription>
Create a new project with the selected services
Choose where to duplicate the selected services
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New project name"
/>
<Label>Duplicate to</Label>
<RadioGroup
value={duplicateType}
onValueChange={setDuplicateType}
className="grid gap-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="new-project" id="new-project" />
<Label htmlFor="new-project">New project</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="same-project" id="same-project" />
<Label htmlFor="same-project">Same project</Label>
</div>
</RadioGroup>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
{duplicateType === "new-project" && (
<>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New project name"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
</>
)}
<div className="grid gap-2">
<Label>Selected services to duplicate</Label>
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Duplicating...
{duplicateType === "new-project"
? "Duplicating project..."
: "Duplicating services..."}
</>
) : duplicateType === "new-project" ? (
"Duplicate project"
) : (
"Duplicate"
"Duplicate services"
)}
</Button>
</DialogFooter>

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { generateSHA256Hash } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -29,7 +30,6 @@ import { toast } from "sonner";
import { z } from "zod";
import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa";
import { Switch } from "@/components/ui/switch";
const profileSchema = z.object({
email: z.string(),

View File

@@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => {
remotely.
</DialogDescription>
</DialogHeader>
<div>
<p className="text-primary text-sm font-medium">
You will need to purchase or rent a Virtual Private Server (VPS) to
proceed, we recommend to use one of these providers since has been
heavily tested.
</p>
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
<li>
<a
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
className="text-link underline"
>
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"
className="text-link underline"
>
DigitalOcean - Get $200 Credits
</a>
</li>
<li>
<a
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
className="text-link underline"
>
Hetzner - Get 20 Credits
</a>
</li>
<li>
<a
href="https://www.vultr.com/?ref=9679828"
className="text-link underline"
>
Vultr
</a>
</li>
<li>
<a
href="https://www.linode.com/es/pricing/#compute-shared"
className="text-link underline"
>
Linode
</a>
</li>
</ul>
<AlertBlock className="mt-4 px-4">
You are free to use whatever provider, but we recommend to use one
of the above, to avoid issues.
</AlertBlock>
</div>
{!canCreateMoreServers && (
<AlertBlock type="warning">
You cannot create more servers,{" "}

View File

@@ -1,7 +1,7 @@
import { useState } from "react";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { useState } from "react";
interface Props {
serverId: string;

View File

@@ -40,10 +40,10 @@ import { HandleServers } from "./handle-servers";
import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSchedulesModal } from "./show-schedules-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowSchedulesModal } from "./show-schedules-modal";
export const ShowServers = () => {
const { t } = useTranslation("settings");

View File

@@ -177,6 +177,14 @@ export const WelcomeSuscription = () => {
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"

View File

@@ -185,24 +185,21 @@ export const ShowInvitations = () => {
Cancel Invitation
</DropdownMenuItem>
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
await removeInvitation({
invitationId: invitation.id,
}).then(() => {
refetch();
toast.success(
"Invitation removed",
);
});
}}
>
Remove Invitation
</DropdownMenuItem>
</>
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
await removeInvitation({
invitationId: invitation.id,
}).then(() => {
refetch();
toast.success("Invitation removed");
});
}}
>
Remove Invitation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>

View File

@@ -1,6 +1,7 @@
import Page from "./side";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { api } from "@/utils/api";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import Page from "./side";
import { ChatwootWidget } from "../shared/ChatwootWidget";
interface Props {
children: React.ReactNode;
@@ -9,10 +10,15 @@ interface Props {
export const DashboardLayout = ({ children }: Props) => {
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<>
<Page>{children}</Page>
{isCloud === true && (
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
)}
{haveRootAccess === true && <ImpersonationBar />}
</>
);

View File

@@ -1,9 +0,0 @@
import Page from "./side";
interface Props {
children: React.ReactNode;
}
export const ProjectLayout = ({ children }: Props) => {
return <Page>{children}</Page>;
};

View File

@@ -0,0 +1,69 @@
import Script from "next/script";
import { useEffect } from "react";
interface ChatwootWidgetProps {
websiteToken: string;
baseUrl?: string;
settings?: {
position?: "left" | "right";
type?: "standard" | "expanded_bubble";
launcherTitle?: string;
darkMode?: boolean;
hideMessageBubble?: boolean;
placement?: "right" | "left";
showPopoutButton?: boolean;
widgetStyle?: "standard" | "bubble";
};
user?: {
identifier: string;
name?: string;
email?: string;
phoneNumber?: string;
avatarUrl?: string;
customAttributes?: Record<string, any>;
identifierHash?: string;
};
}
export const ChatwootWidget = ({
websiteToken,
baseUrl = "https://app.chatwoot.com",
settings = {
position: "right",
type: "standard",
launcherTitle: "Chat with us",
},
user,
}: ChatwootWidgetProps) => {
useEffect(() => {
// Configurar los settings de Chatwoot
window.chatwootSettings = {
position: "right",
};
(window as any).chatwootSDKReady = () => {
window.chatwootSDK?.run({ websiteToken, baseUrl });
const trySetUser = () => {
if (window.$chatwoot && user) {
window.$chatwoot.setUser(user.identifier, {
email: user.email,
name: user.name,
avatar_url: user.avatarUrl,
phone_number: user.phoneNumber,
});
}
};
trySetUser();
};
}, [websiteToken, baseUrl, user, settings]);
return (
<Script
src={`${baseUrl}/packs/js/sdk.js`}
strategy="lazyOnload"
onLoad={() => (window as any).chatwootSDKReady?.()}
/>
);
};

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "isStaticSpa" boolean;

File diff suppressed because it is too large Load Diff

View File

@@ -645,6 +645,13 @@
"when": 1746518402168,
"tag": "0091_spotty_kulan_gath",
"breakpoints": true
},
{
"idx": 92,
"version": "7",
"when": 1747713229160,
"tag": "0092_stiff_the_watchers",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.22.3",
"version": "v0.22.7",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -36,6 +36,8 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
"@ai-sdk/cohere": "^1.0.6",
@@ -186,7 +188,7 @@
},
"packageManager": "pnpm@9.5.0",
"engines": {
"node": "^20.9.0",
"node": "^20.16.0",
"pnpm": ">=9.5.0"
},
"lint-staged": {

View File

@@ -17,7 +17,6 @@ const inter = Inter({ subsets: ["latin"] });
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
// session: Session | null;
theme?: string;
};
@@ -33,11 +32,13 @@ const MyApp = ({
return (
<>
<style jsx global>{`
:root {
--font-inter: ${inter.style.fontFamily};
}
`}</style>
<style jsx global>
{`
:root {
--font-inter: ${inter.style.fontFamily};
}
`}
</style>
<Head>
<title>Dokploy</title>
</Head>

View File

@@ -80,7 +80,13 @@ export default function Custom404({ statusCode, error }: Props) {
<footer className="mt-auto text-center py-5">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-sm text-gray-500">
Submit Log in issue on Github
<Link
href="https://github.com/Dokploy/dokploy/issues"
target="_blank"
className="underline hover:text-primary transition-colors"
>
Submit Log in issue on Github
</Link>
</p>
</div>
</footer>

View File

@@ -10,7 +10,7 @@ import {
PostgresqlIcon,
RedisIcon,
} from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -18,6 +18,7 @@ import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button";
import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
import {
Card,
CardContent,
@@ -93,7 +94,6 @@ import { useRouter } from "next/router";
import { type ReactElement, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
export type Services = {
appName: string;
@@ -1064,7 +1064,7 @@ const Project = (
export default Project;
Project.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -18,7 +18,7 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -370,7 +370,7 @@ const Service = (
export default Service;
Service.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -13,7 +13,7 @@ import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -217,12 +217,12 @@ const Service = (
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
"xl:grid xl:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "lg:grid-cols-9"
? "xl:grid-cols-9"
: data?.serverId
? "lg:grid-cols-8"
: "lg:grid-cols-9",
? "xl:grid-cols-8"
: "xl:grid-cols-9",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
@@ -366,7 +366,7 @@ const Service = (
export default Service;
Service.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -294,7 +294,7 @@ const Mariadb = (
export default Mariadb;
Mariadb.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -296,7 +296,7 @@ const Mongo = (
export default Mongo;
Mongo.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/gener
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -280,7 +280,7 @@ const MySql = (
export default MySql;
MySql.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -278,7 +278,7 @@ const Postgresql = (
export default Postgresql;
Postgresql.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -9,7 +9,7 @@ import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/gener
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { RedisIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -285,7 +285,7 @@ const Redis = (
export default Redis;
Redis.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -1,11 +1,11 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import type { ReactElement } from "react";
import type { GetServerSidePropsContext } from "next";
import { validateRequest } from "@dokploy/server/lib/auth";
import { IS_CLOUD } from "@dokploy/server/constants";
import { api } from "@/utils/api";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
function SchedulesPage() {
const { data: user } = api.user.get.useQuery();
return (

View File

@@ -1,16 +1,16 @@
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { api } from "@/utils/api";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { Card } from "@/components/ui/card";
const Page = () => {
const { data: user } = api.user.get.useQuery();
return (

View File

@@ -330,6 +330,7 @@ export const applicationRouter = createTRPCRouter({
dockerContextPath: input.dockerContextPath,
dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion,
isStaticSpa: input.isStaticSpa,
});
return true;

View File

@@ -51,9 +51,9 @@ import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import { parse } from "toml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { parse } from "toml";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
@@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({
}
const projectName = slugify(`${project.name} ${input.id}`);
const generate = processTemplate(template.config, {
const appName = `${projectName}-${generatePassword(6)}`;
const config = {
...template.config,
variables: {
APP_NAME: appName,
...template.config.variables,
},
};
const generate = processTemplate(config, {
serverIp: serverIp,
projectName: projectName,
});
@@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({
serverId: input.serverId,
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
appName: appName,
isolatedDeployment: true,
});
@@ -605,7 +613,15 @@ export const composeRouter = createTRPCRouter({
});
}
const processedTemplate = processTemplate(config, {
const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp,
projectName: compose.appName,
});
@@ -675,7 +691,15 @@ export const composeRouter = createTRPCRouter({
});
}
const processedTemplate = processTemplate(config, {
const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp,
projectName: compose.appName,
});

View File

@@ -1,3 +1,4 @@
import { db } from "@/server/db";
import {
apiFindAllByApplication,
apiFindAllByCompose,
@@ -16,7 +17,6 @@ import {
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@/server/db";
export const deploymentRouter = createTRPCRouter({
all: protectedProcedure

View File

@@ -21,32 +21,32 @@ import {
addNewProject,
checkProjectAccess,
createApplication,
createBackup,
createCompose,
createDomain,
createMariadb,
createMongo,
createMount,
createMysql,
createPort,
createPostgres,
createPreviewDeployment,
createProject,
createRedirect,
createRedis,
createSecurity,
deleteProject,
findApplicationById,
findComposeById,
findMongoById,
findMariadbById,
findMemberById,
findRedisById,
findMongoById,
findMySqlById,
findPostgresById,
findProjectById,
findRedisById,
findUserById,
updateProjectById,
findPostgresById,
findMariadbById,
findMySqlById,
createDomain,
createPort,
createMount,
createRedirect,
createPreviewDeployment,
createBackup,
createSecurity,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, sql } from "drizzle-orm";
@@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({
}),
)
.optional(),
duplicateInSameProject: z.boolean().default(false),
}),
)
.mutation(async ({ ctx, input }) => {
@@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({
});
}
// Create new project
const newProject = await createProject(
{
name: input.name,
description: input.description,
env: sourceProject.env,
},
ctx.session.activeOrganizationId,
);
// Create new project or use existing one
const targetProject = input.duplicateInSameProject
? sourceProject
: await createProject(
{
name: input.name,
description: input.description,
env: sourceProject.env,
},
ctx.session.activeOrganizationId,
);
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
@@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({
const newApplication = await createApplication({
...application,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${application.name} (copy)`
: application.name,
projectId: targetProject.projectId,
});
for (const domain of domains) {
@@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({
const newPostgres = await createPostgres({
...postgres,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({
await findMariadbById(id);
const newMariadb = await createMariadb({
...mariadb,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mariadb.name} (copy)`
: mariadb.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({
await findMongoById(id);
const newMongo = await createMongo({
...mongo,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mongo.name} (copy)`
: mongo.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({
await findMySqlById(id);
const newMysql = await createMysql({
...mysql,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mysql.name} (copy)`
: mysql.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({
const { redisId, mounts, ...redis } = await findRedisById(id);
const newRedis = await createRedis({
...redis,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${redis.name} (copy)`
: redis.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({
await findComposeById(id);
const newCompose = await createCompose({
...compose,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({
};
// Duplicate selected services
for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type);
}
}
if (ctx.user.role === "member") {
if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
newProject.projectId,
targetProject.projectId,
ctx.session.activeOrganizationId,
);
}
return newProject;
return targetProject;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",

View File

@@ -1,24 +1,24 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { removeJob, schedule } from "@/server/utils/backup";
import { IS_CLOUD, scheduleJob } from "@dokploy/server";
import { removeScheduleJob } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { deployments } from "@dokploy/server/db/schema/deployment";
import {
createScheduleSchema,
schedules,
updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule";
import { desc, eq } from "drizzle-orm";
import { db } from "@dokploy/server/db";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { runCommand } from "@dokploy/server/index";
import { deployments } from "@dokploy/server/db/schema/deployment";
import {
createSchedule,
deleteSchedule,
findScheduleById,
createSchedule,
updateSchedule,
} from "@dokploy/server/services/schedule";
import { IS_CLOUD, scheduleJob } from "@dokploy/server";
import { removeJob, schedule } from "@/server/utils/backup";
import { removeScheduleJob } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const scheduleRouter = createTRPCRouter({
create: protectedProcedure
.input(createScheduleSchema)

View File

@@ -1,9 +1,9 @@
import type http from "node:http";
import { validateRequest } from "@dokploy/server/lib/auth";
import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { WebSocketServer } from "ws";
import { appRouter } from "../api/root";
import { createTRPCContext } from "../api/trpc";
import { validateRequest } from "@dokploy/server/lib/auth";
export const setupDrawerLogsWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,

39
apps/dokploy/types/chatwoot.d.ts vendored Normal file
View File

@@ -0,0 +1,39 @@
declare global {
interface Window {
chatwootSettings?: {
hideMessageBubble?: boolean;
showUnreadMessagesDialog?: boolean;
position?: "left" | "right";
locale?: string;
useBrowserLanguage?: boolean;
type?: "standard" | "expanded_bubble";
darkMode?: "light" | "auto";
launcherTitle?: string;
showPopoutButton?: boolean;
baseDomain?: string;
};
chatwootSDK?: {
run: (config: {
websiteToken: string;
baseUrl: string;
}) => void;
};
$chatwoot?: {
setUser: (
identifier: string,
userAttributes: Record<string, any>,
) => void;
setCustomAttributes: (attributes: Record<string, any>) => void;
reset: () => void;
toggle: (state?: "open" | "close") => void;
popoutChatWindow: () => void;
toggleBubbleVisibility: (visibility: "show" | "hide") => void;
setLocale: (locale: string) => void;
setLabel: (label: string) => void;
removeLabel: (label: string) => void;
};
chatwootSDKReady?: () => void;
}
}
export {};

View File

@@ -7,11 +7,11 @@ import {
findServerById,
keepLatestNBackups,
runCommand,
runComposeBackup,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
runPostgresBackup,
runComposeBackup,
} from "@dokploy/server";
import { db } from "@dokploy/server/dist/db";
import { backups, schedules, server } from "@dokploy/server/dist/db/schema";

View File

@@ -32,7 +32,7 @@
},
"packageManager": "pnpm@9.5.0",
"engines": {
"node": "^20.9.0",
"node": "^20.16.0",
"pnpm": ">=9.5.0"
},
"lint-staged": {

View File

@@ -28,6 +28,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"micromatch": "4.0.8",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",

View File

@@ -208,6 +208,7 @@ export const applications = pgTable("application", {
buildType: buildType("buildType").notNull().default("nixpacks"),
herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -412,6 +413,7 @@ const createSchema = createInsertSchema(applications, {
]),
herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(),
isStaticSpa: z.boolean().optional(),
owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
@@ -464,7 +466,7 @@ export const apiSaveBuildType = createSchema
herokuVersion: true,
})
.required()
.merge(createSchema.pick({ publishDirectory: true }));
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));
export const apiSaveGithubProvider = createSchema
.pick({

View File

@@ -11,15 +11,15 @@ import {
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { generateAppName } from ".";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { users_temp } from "./user";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { generateAppName } from ".";
export const databaseType = pgEnum("databaseType", [
"postgres",
"mariadb",

View File

@@ -3,6 +3,7 @@ import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { backups } from "./backups";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
@@ -15,7 +16,6 @@ import { server } from "./server";
import { applicationStatus, triggerType } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
import { backups } from "./backups";
import { schedules } from "./schedule";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [

View File

@@ -10,11 +10,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { backups } from "./backups";
import { compose } from "./compose";
import { previewDeployments } from "./preview-deployments";
import { server } from "./server";
import { schedules } from "./schedule";
import { backups } from "./backups";
import { server } from "./server";
export const deploymentStatus = pgEnum("deploymentStatus", [
"running",
"done",

View File

@@ -4,11 +4,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { deployments } from "./deployment";
import { generateAppName } from "./utils";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { server } from "./server";
import { users_temp } from "./user";
import { generateAppName } from "./utils";
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
export const scheduleType = pgEnum("scheduleType", [

View File

@@ -20,9 +20,9 @@ import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis";
import { schedules } from "./schedule";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
import { schedules } from "./schedule";
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
export const server = pgTable("server", {

View File

@@ -11,10 +11,10 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { account, apikey, organization } from "./account";
import { projects } from "./project";
import { certificateType } from "./shared";
import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule";
import { certificateType } from "./shared";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.

View File

@@ -132,3 +132,5 @@ export {
export * from "./utils/schedules/utils";
export * from "./utils/schedules/index";
export * from "./lib/logger";

View File

@@ -3,7 +3,7 @@ import * as bcrypt from "bcrypt";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { APIError } from "better-auth/api";
import { apiKey, organization, twoFactor, admin } from "better-auth/plugins";
import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { db } from "../db";

View File

@@ -0,0 +1,11 @@
import pino from "pino";
export const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
levelFirst: false,
},
},
});

View File

@@ -24,13 +24,13 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
import { type Server, findServerById } from "./server";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { findBackupById } from "./backup";
import {
type PreviewDeployment,
findPreviewDeploymentById,
updatePreviewDeployment,
} from "./preview-deployment";
import { findScheduleById } from "./schedule";
import { findBackupById } from "./backup";
export type Deployment = typeof deployments.$inferSelect;

View File

@@ -1,3 +1,5 @@
import dns from "node:dns";
import { promisify } from "node:util";
import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
@@ -7,8 +9,6 @@ import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { findServerById } from "./server";
import dns from "node:dns";
import { promisify } from "node:util";
export type Domain = typeof domains.$inferSelect;

View File

@@ -1,16 +1,16 @@
import { type Schedule, schedules } from "../db/schema/schedule";
import { db } from "../db";
import { eq } from "drizzle-orm";
import path from "node:path";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import type { z } from "zod";
import { paths } from "../constants";
import { db } from "../db";
import { type Schedule, schedules } from "../db/schema/schedule";
import type {
createScheduleSchema,
updateScheduleSchema,
} from "../db/schema/schedule";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { paths } from "../constants";
import path from "node:path";
import { encodeBase64 } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;

View File

@@ -356,20 +356,20 @@ const installUtilities = () => `
case "$OS_TYPE" in
arch)
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
;;
alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git jq openssl sudo unzip tar >/dev/null
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
;;
ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq openssl >/dev/null
dnf install -y wget git git-lfs jq openssl >/dev/null
else
if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null
@@ -377,12 +377,12 @@ const installUtilities = () => `
if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null
fi
dnf install -y wget git jq openssl unzip >/dev/null
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
fi
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null
zypper install -y curl wget git jq openssl >/dev/null
zypper install -y curl wget git git-lfs jq openssl >/dev/null
;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
export NIXPACKS_VERSION=1.35.0
export NIXPACKS_VERSION=1.39.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi

View File

@@ -139,6 +139,8 @@ export const initializeTraefik = async ({
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik container started successfully after retry");
} else {
throw error;
}
}
} catch (error) {

View File

@@ -1,4 +1,4 @@
import { randomBytes, createHmac } from "node:crypto";
import { createHmac, randomBytes } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -35,7 +35,7 @@ export const generateRandomDomain = ({
projectName,
}: Schema): string => {
const hash = randomBytes(3).toString("hex");
const slugIp = serverIp.replaceAll(".", "-");
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
};

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Compose } from "@dokploy/server/services/compose";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path, getBackupCommand } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runComposeBackup = async (
compose: Compose,

View File

@@ -11,10 +11,10 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, scheduleBackup } from "./utils";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { startLogCleanup } from "../access-log/handler";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { eq } from "drizzle-orm";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMariadbBackup = async (
mariadb: Mariadb,

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { projectId, name } = mongo;

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { projectId, name } = mysql;

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
export const runPostgresBackup = async (
postgres: Postgres,

View File

@@ -1,13 +1,14 @@
import { logger } from "@dokploy/server/lib/logger";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Destination } from "@dokploy/server/services/destination";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { keepLatestNBackups } from ".";
import { runComposeBackup } from "./compose";
import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres";
import { runWebServerBackup } from "./web-server";
import { runComposeBackup } from "./compose";
export const scheduleBackup = (backup: BackupSchedule) => {
const {
@@ -222,6 +223,17 @@ export const getBackupCommand = (
) => {
const containerSearch = getContainerSearchCommand(backup);
const backupCommand = generateBackupCommand(backup);
logger.info(
{
containerSearch,
backupCommand,
rcloneCommand,
logPath,
},
`Executing backup command: ${backup.databaseType} ${backup.backupType}`,
);
return `
set -eo pipefail;
echo "[$(date)] Starting backup process..." >> ${logPath};

View File

@@ -1,16 +1,16 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils";
import { findDestinationById } from "@dokploy/server/services/destination";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import { mkdtemp } from "node:fs/promises";
import { join } from "node:path";
import { createWriteStream } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { createWriteStream } from "node:fs";
import { findDestinationById } from "@dokploy/server/services/destination";
import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils";
export const runWebServerBackup = async (backup: BackupSchedule) => {
if (IS_CLOUD) {
@@ -51,13 +51,23 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
const postgresContainerId = containerId.trim();
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
// First dump the database inside the container
const dumpCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy -f /tmp/database.sql`;
writeStream.write(`Running dump command: ${dumpCommand}\n`);
await execAsync(dumpCommand);
writeStream.write(`Running command: ${postgresCommand}\n`);
await execAsync(postgresCommand);
// Then copy the file from the container to host
const copyCommand = `docker cp ${postgresContainerId}:/tmp/database.sql ${tempDir}/database.sql`;
writeStream.write(`Copying database dump: ${copyCommand}\n`);
await execAsync(copyCommand);
// Clean up the temp file in the container
const cleanupCommand = `docker exec ${postgresContainerId} rm -f /tmp/database.sql`;
writeStream.write(`Cleaning up temp file: ${cleanupCommand}\n`);
await execAsync(cleanupCommand);
await execAsync(
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
);
writeStream.write("Copied filesystem to temp directory\n");
@@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await updateDeploymentStatus(deployment.deploymentId, "done");
return true;
} finally {
await execAsync(`rm -rf ${tempDir}`);
try {
await rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
console.error("Cleanup error:", cleanupError);
}
}
} catch (error) {
console.error("Backup error:", error);

View File

@@ -190,7 +190,8 @@ const createEnvFile = (compose: ComposeNested) => {
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || "";
let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
}
@@ -219,7 +220,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || "";
let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
}

View File

@@ -2,7 +2,10 @@ import { createHash } from "node:crypto";
import type { WriteStream } from "node:fs";
import { nanoid } from "nanoid";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import {
parseEnvironmentKeyValuePair,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { execAsync } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
@@ -81,10 +84,10 @@ export const buildRailpack = async (
// Add secrets properly formatted
const env: { [key: string]: string } = {};
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
for (const pair of envVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env='${key}'`);
buildArgs.push("--secret", `id=${key},env=${key}`);
env[key] = value;
}
}
@@ -161,11 +164,11 @@ export const getRailpackCommand = (
// Add secrets properly formatted
const exportEnvs = [];
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
for (const pair of envVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env='${key}'`);
exportEnvs.push(`export ${key}=${value}`);
buildArgs.push("--secret", `id=${key},env=${key}`);
exportEnvs.push(`export ${key}='${value}'`);
}
}

View File

@@ -7,21 +7,58 @@ import type { ApplicationNested } from ".";
import { createFile, getCreateFileCommand } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
const nginxSpaConfig = `
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
error_log /dev/stderr;
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
}
`;
export const buildStatic = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { publishDirectory } = application;
const { publishDirectory, isStaticSpa } = application;
const buildAppDirectory = getBuildAppDirectory(application);
try {
if (isStaticSpa) {
createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig);
}
createFile(
buildAppDirectory,
".dockerignore",
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
);
createFile(
buildAppDirectory,
"Dockerfile",
[
"FROM nginx:alpine",
"WORKDIR /usr/share/nginx/html/",
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
`COPY ${publishDirectory || "."} .`,
'CMD ["nginx", "-g", "daemon off;"]',
].join("\n"),
);

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import type { Readable } from "node:stream";
import { docker, paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import type { ContainerInfo, ResourceRequirements } from "dockerode";
import { parse } from "dotenv";
import type { ApplicationNested } from "../builders";
@@ -13,7 +14,6 @@ import type { RedisNested } from "../databases/redis";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import { getRemoteDocker } from "../servers/remote-docker";
import type { Compose } from "@dokploy/server/services/compose";
interface RegistryAuth {
username: string;
@@ -279,6 +279,17 @@ export const prepareEnvironmentVariables = (
return resolvedVars;
};
export const parseEnvironmentKeyValuePair = (
pair: string,
): [string, string] => {
const [key, ...valueParts] = pair.split("=");
if (!key || !valueParts.length) {
throw new Error(`Invalid environment variable pair: ${pair}`);
}
return [key, valueParts.join("")];
};
export const getEnviromentVariablesObject = (
input: string | null,
projectEnv?: string | null,
@@ -288,7 +299,7 @@ export const getEnviromentVariablesObject = (
const jsonObject: Record<string, string> = {};
for (const pair of envs) {
const [key, value] = pair.split("=");
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
jsonObject[key] = value;
}

View File

@@ -246,32 +246,18 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
const allProjects = await validateGitlabProvider(gitlabProvider);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const filteredRepos = repositories.filter((repo: any) => {
const filteredRepos = allProjects.filter((repo: any) => {
const { full_path, kind } = repo.namespace;
const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) {
const isIncluded = groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
.some((name) =>
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
);
return isIncluded && kind === "group";
}
@@ -432,23 +418,7 @@ export const testGitlabConnection = async (
const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const repositories = await validateGitlabProvider(gitlabProvider);
const filteredRepos = repositories.filter((repo: any) => {
const { full_path, kind } = repo.namespace;
@@ -456,10 +426,56 @@ export const testGitlabConnection = async (
if (groupName) {
return groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
.some((name) =>
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
);
}
return kind === "user";
});
return filteredRepos.length;
};
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
try {
const allProjects = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
while (true) {
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const projects = await response.json();
if (projects.length === 0) {
break;
}
allProjects.push(...projects);
page++;
const total = response.headers.get("x-total");
if (total && allProjects.length >= Number.parseInt(total)) {
break;
}
}
return allProjects;
} catch (error) {
throw error;
}
};

Some files were not shown because too many files have changed in this diff Show More