diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml index 529cd8f7f..542944611 100644 --- a/.github/workflows/dokploy.yml +++ b/.github/workflows/dokploy.yml @@ -138,6 +138,8 @@ jobs: needs: [combine-manifests] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + outputs: + version: ${{ steps.get_version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v4 @@ -160,3 +162,80 @@ jobs: prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + sync-version: + needs: [generate-release] + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync version to MCP repository + run: | + git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo + cd /tmp/mcp-repo + + jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp + mv package.json.tmp package.json + + npm install -g pnpm + pnpm install + pnpm run fetch-openapi + pnpm run generate + + git config user.name "Dokploy Bot" + git config user.email "bot@dokploy.com" + git add -A + git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \ + -m "Source: ${{ github.repository }}@${{ github.sha }}" \ + --allow-empty + git push + + echo "✅ MCP repo synced to version ${{ needs.generate-release.outputs.version }}" + + - name: Sync version to CLI repository + run: | + git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo + cd /tmp/cli-repo + + jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp + mv package.json.tmp package.json + + cp ${{ github.workspace }}/openapi.json ./openapi.json + npm install -g pnpm + pnpm install + pnpm run generate + + git config user.name "Dokploy Bot" + git config user.email "bot@dokploy.com" + git add -A + git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \ + -m "Source: ${{ github.repository }}@${{ github.sha }}" \ + --allow-empty + git push + + echo "✅ CLI repo synced to version ${{ needs.generate-release.outputs.version }}" + + - name: Sync version to SDK repository + run: | + git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/sdk.git /tmp/sdk-repo + cd /tmp/sdk-repo + + jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp + mv package.json.tmp package.json + + cp ${{ github.workspace }}/openapi.json ./openapi.json + npm install -g pnpm + pnpm install + pnpm run generate + + git config user.name "Dokploy Bot" + git config user.email "bot@dokploy.com" + git add -A + git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \ + -m "Source: ${{ github.repository }}@${{ github.sha }}" \ + --allow-empty + git push + + echo "✅ SDK repo synced to version ${{ needs.generate-release.outputs.version }}" diff --git a/.github/workflows/sync-openapi-docs.yml b/.github/workflows/sync-openapi-docs.yml index 1a6b1e87b..147d8d97e 100644 --- a/.github/workflows/sync-openapi-docs.yml +++ b/.github/workflows/sync-openapi-docs.yml @@ -110,3 +110,24 @@ jobs: echo "✅ OpenAPI synced to CLI repository successfully" + - name: Sync to SDK repository + run: | + git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/sdk.git sdk-repo + + cd sdk-repo + + cp -f ../openapi.json openapi.json + + git config user.name "Dokploy Bot" + git config user.email "bot@dokploy.com" + + git add openapi.json + git commit -m "chore: sync OpenAPI specification [skip ci]" \ + -m "Source: ${{ github.repository }}@${{ github.sha }}" \ + -m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \ + --allow-empty + + git push + + echo "✅ OpenAPI synced to SDK repository successfully" + diff --git a/.github/workflows/sync-version.yml b/.github/workflows/sync-version.yml deleted file mode 100644 index 5e8ccb706..000000000 --- a/.github/workflows/sync-version.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Sync version to MCP and CLI repos - -on: - release: - types: [published] - push: - tags: - - 'v*' - workflow_dispatch: - -jobs: - sync-version: - name: Sync version to external repos - runs-on: ubuntu-latest - steps: - - name: Checkout Dokploy repository - uses: actions/checkout@v4 - - - name: Get version - id: get_version - run: | - VERSION=$(jq -r .version apps/dokploy/package.json | sed 's/^v//') - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - - - name: Sync version to MCP repository - run: | - git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo - cd /tmp/mcp-repo - - # Regenerate tools from latest OpenAPI spec - npm install -g pnpm - pnpm install - pnpm run fetch-openapi - pnpm run generate - - # Bump version after install so pnpm install doesn't overwrite it - jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp - mv package.json.tmp package.json - - git config user.name "Dokploy Bot" - git config user.email "bot@dokploy.com" - - git add -A - git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \ - -m "Source: ${{ github.repository }}@${{ github.sha }}" \ - -m "Release: ${{ github.event.release.html_url }}" \ - --allow-empty - - git push - - - - name: Sync version to CLI repository - run: | - git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo - - cd /tmp/cli-repo - - # Copy latest openapi spec and regenerate commands - cp ${{ github.workspace }}/openapi.json ./openapi.json - npm install -g pnpm - pnpm install - pnpm run generate - - # Bump version after install so pnpm install doesn't overwrite it - if [ -f package.json ]; then - jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp - mv package.json.tmp package.json - fi - - git config user.name "Dokploy Bot" - git config user.email "bot@dokploy.com" - - git add -A - git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \ - -m "Source: ${{ github.repository }}@${{ github.sha }}" \ - -m "Release: ${{ github.event.release.html_url }}" \ - --allow-empty - - git push - - echo "CLI repo synced to version ${{ steps.get_version.outputs.version }}" - diff --git a/.vscode/settings.json b/.vscode/settings.json index 99357f236..463ce8e24 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "editor.codeActionsOnSave": { "source.fixAll.biome": "explicit", "source.organizeImports.biome": "explicit" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts index 202abdf2d..52be798d7 100644 --- a/apps/dokploy/__test__/templates/config.template.test.ts +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -494,4 +494,49 @@ describe("processTemplate", () => { expect(result.mounts).toHaveLength(1); }); }); + + describe("isolated deployment config", () => { + it("should default to isolated=true when not specified", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: {}, + }, + }; + + expect(template.config.isolated).toBeUndefined(); + // undefined !== false => isolatedDeployment = true + expect(template.config.isolated !== false).toBe(true); + }); + + it("should be isolated when isolated=true is explicitly set", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + isolated: true, + domains: [], + env: {}, + }, + }; + + expect(template.config.isolated !== false).toBe(true); + }); + + it("should disable isolated deployment when isolated=false", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + isolated: false, + domains: [], + env: {}, + }, + }; + + expect(template.config.isolated !== false).toBe(false); + }); + }); }); diff --git a/apps/dokploy/__test__/templates/helpers.template.test.ts b/apps/dokploy/__test__/templates/helpers.template.test.ts index f2af2717b..e95bbf072 100644 --- a/apps/dokploy/__test__/templates/helpers.template.test.ts +++ b/apps/dokploy/__test__/templates/helpers.template.test.ts @@ -30,9 +30,7 @@ describe("helpers functions", () => { const domain = processValue("${domain}", {}, mockSchema); expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); expect( - domain.endsWith( - `${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`, - ), + domain.endsWith(`${mockSchema.serverIp.replaceAll(".", "-")}.sslip.io`), ).toBeTruthy(); }); }); diff --git a/apps/dokploy/components/dashboard/application/domains/columns.tsx b/apps/dokploy/components/dashboard/application/domains/columns.tsx index cd8254aa0..b88443dcc 100644 --- a/apps/dokploy/components/dashboard/application/domains/columns.tsx +++ b/apps/dokploy/components/dashboard/application/domains/columns.tsx @@ -21,9 +21,9 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import type { RouterOutputs } from "@/utils/api"; -import type { ValidationStates } from "./show-domains"; -import { AddDomain } from "./handle-domain"; import { DnsHelperModal } from "./dns-helper-modal"; +import { AddDomain } from "./handle-domain"; +import type { ValidationStates } from "./show-domains"; export type Domain = | RouterOutputs["domain"]["byApplicationId"][0] @@ -168,7 +168,7 @@ export const createColumns = ({ {domain.certificateType} )} - {!domain.host.includes("traefik.me") && ( + {!domain.host.includes("sslip.io") && ( @@ -256,7 +256,7 @@ export const createColumns = ({ return (
- {!domain.host.includes("traefik.me") && ( + {!domain.host.includes("sslip.io") && ( { const https = form.watch("https"); const domainType = form.watch("domainType"); const host = form.watch("host"); - const isTraefikMeDomain = host?.includes("traefik.me") || false; + const isTraefikMeDomain = host?.includes("sslip.io") || false; useEffect(() => { if (data) { @@ -513,7 +513,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { render={({ field }) => ( {!canGenerateTraefikMeDomains && - field.value.includes("traefik.me") && ( + field.value.includes("sslip.io") && ( You need to set an IP address in your{" "} { ? "Remote Servers -> Server -> Edit Server -> Update IP Address" : "Web Server -> Server -> Update Server IP"} {" "} - to make your traefik.me domain work. + to make your sslip.io domain work. )} {isTraefikMeDomain && ( - Note: traefik.me is a public HTTP + Note: sslip.io is a public HTTP service and does not support SSL/HTTPS. HTTPS and certificate options will not have any effect. @@ -567,7 +567,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { sideOffset={5} className="max-w-[10rem]" > -

Generate traefik.me domain

+

Generate sslip.io domain

diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index e971f9ab7..af8d691c0 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -425,7 +425,7 @@ export const ShowDomains = ({ id, type }: Props) => { )}
- {!item.host.includes("traefik.me") && ( + {!item.host.includes("sslip.io") && ( { return (
- -
-
-
- ( - -
- Repository URL - {field.value?.startsWith("https://") && ( - - - View Repository - - )} -
- - - - -
- )} - /> -
- {sshKeys && sshKeys.length > 0 ? ( - ( - - - SSH Key - - - - - - - )} - /> - ) : ( - + +
+ ( + +
+ Repository URL + {field.value?.startsWith("https://") && ( + + + View Repository + + )} +
+ + + + +
)} -
-
+ /> + {sshKeys && sshKeys.length > 0 ? ( ( - - Branch + + + SSH Key + + - + - )} /> -
+ ) : ( + + )} + + ( + + Branch + + + + + + )} + /> ( - + Build Path @@ -223,7 +220,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { control={form.control} name="watchPaths" render={({ field }) => ( - +
Watch Paths diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx index 02cae2c4a..0781f55a8 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -72,7 +73,10 @@ const GiteaProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), giteaId: z.string().min(1, "Gitea Provider is required"), watchPaths: z.array(z.string()).default([]), enableSubmodules: z.boolean().optional(), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index 6bce2d243..b4f069ee2 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GithubIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -55,7 +56,10 @@ const GithubProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), triggerType: z.enum(["push", "tag"]).default("push"), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx index b49a1658f..d867654b5 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -58,7 +59,10 @@ const GitlabProviderSchema = z.object({ id: z.number().nullable(), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), gitlabId: z.string().min(1, "Gitlab Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index 01fc9e84a..474d75201 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -58,7 +58,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { Deploy Settings - + {canDeploy && ( { > {canUpdateService && ( -
+
Autodeploy { )} {canUpdateService && ( -
+
Clean Cache { if (data) { @@ -162,7 +162,7 @@ export const AddPreviewDomain = ({ {isTraefikMeDomain && ( - Note: traefik.me is a public HTTP + Note: sslip.io is a public HTTP service and does not support SSL/HTTPS. HTTPS and certificate options will not have any effect. @@ -202,7 +202,7 @@ export const AddPreviewDomain = ({ sideOffset={5} className="max-w-[10rem]" > -

Generate traefik.me domain

+

Generate sslip.io domain

diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index d2840cd67..13b9a1603 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -88,7 +88,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { const form = useForm({ defaultValues: { env: "", - wildcardDomain: "*.traefik.me", + wildcardDomain: "*.sslip.io", port: 3000, previewLimit: 3, previewLabels: [], @@ -102,7 +102,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { const previewHttps = form.watch("previewHttps"); const wildcardDomain = form.watch("wildcardDomain"); - const isTraefikMeDomain = wildcardDomain?.includes("traefik.me") || false; + const isTraefikMeDomain = wildcardDomain?.includes("sslip.io") || false; useEffect(() => { setIsEnabled(data?.isPreviewDeploymentsActive || false); @@ -114,7 +114,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { env: data.previewEnv || "", buildArgs: data.previewBuildArgs || "", buildSecrets: data.previewBuildSecrets || "", - wildcardDomain: data.previewWildcard || "*.traefik.me", + wildcardDomain: data.previewWildcard || "*.sslip.io", port: data.previewPort || 3000, previewLabels: data.previewLabels || [], previewLimit: data.previewLimit || 3, @@ -173,7 +173,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
{isTraefikMeDomain && ( - Note: traefik.me is a public HTTP service and + Note: sslip.io is a public HTTP service and does not support SSL/HTTPS. HTTPS and certificate options will not have any effect. @@ -192,7 +192,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { Wildcard Domain - + diff --git a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx index 36ddb53f1..3fd3089de 100644 --- a/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/handle-schedules.tsx @@ -80,6 +80,7 @@ export const commonCronExpressions = [ const formSchema = z .object({ name: z.string().min(1, "Name is required"), + description: z.string().optional(), cronExpression: z.string().min(1, "Cron expression is required"), shellType: z.enum(["bash", "sh"]).default("bash"), command: z.string(), @@ -224,6 +225,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { resolver: standardSchemaResolver(formSchema), defaultValues: { name: "", + description: "", cronExpression: "", shellType: "bash", command: "", @@ -263,6 +265,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { if (scheduleId && schedule) { form.reset({ name: schedule.name, + description: schedule.description || "", cronExpression: schedule.cronExpression, shellType: schedule.shellType, command: schedule.command, @@ -479,6 +482,26 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { )} /> + ( + + Description + + + + + Optional description of what this schedule does + + + + )} + /> + { {schedule.enabled ? "Enabled" : "Disabled"}
+ {schedule.description && ( +

+ {schedule.description} +

+ )}
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx index 3e099251e..745f72d3b 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -57,7 +58,10 @@ const BitbucketProviderSchema = z.object({ slug: z.string().optional(), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx index 7878225a9..7ebb7edb9 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx @@ -6,6 +6,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -41,7 +42,10 @@ const GitProviderSchema = z.object({ repositoryURL: z.string().min(1, { message: "Repository URL is required", }), - branch: z.string().min(1, "Branch required"), + branch: z + .string() + .min(1, "Branch required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx index 7ea71fc89..7515af723 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx @@ -1,10 +1,11 @@ import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { CheckIcon, ChevronsUpDown, Plus, X, HelpCircle } from "lucide-react"; +import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; import Link from "next/link"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -57,7 +58,10 @@ const GiteaProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), giteaId: z.string().min(1, "Gitea Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx index 827ce1a8a..10075fb5c 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx @@ -1,3 +1,4 @@ +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react"; import Link from "next/link"; @@ -55,7 +56,10 @@ const GithubProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), triggerType: z.enum(["push", "tag"]).default("push"), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx index 63de87d8f..a81774fec 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -58,7 +59,10 @@ const GitlabProviderSchema = z.object({ gitlabPathNamespace: z.string().min(1), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), gitlabId: z.string().min(1, "Gitlab Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 7b212acb9..8ae6453fd 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -288,7 +288,6 @@ export const RestoreBackup = ({ toast.error("Please select a database type"); return; } - console.log({ data }); setIsDeploying(true); }; diff --git a/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx index 267735eac..1b8ec736a 100644 --- a/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/analyze-logs.tsx @@ -1,5 +1,14 @@ "use client"; -import { Bot, Loader2, RotateCcw, Settings, X } from "lucide-react"; +import copy from "copy-to-clipboard"; +import { + Bot, + Check, + Copy, + Loader2, + RotateCcw, + Settings, + X, +} from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import ReactMarkdown from "react-markdown"; @@ -30,6 +39,7 @@ const MAX_LOG_LINES = 200; export function AnalyzeLogs({ logs, context }: Props) { const [open, setOpen] = useState(false); const [aiId, setAiId] = useState(""); + const [copied, setCopied] = useState(false); const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, { enabled: open, }); @@ -52,6 +62,15 @@ export function AnalyzeLogs({ logs, context }: Props) { mutate({ aiId, logs: logsText, context }); }; + const handleCopy = () => { + if (!data?.analysis) return; + const success = copy(data.analysis); + if (success) { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + return ( - + AI @@ -168,6 +187,18 @@ export function AnalyzeLogs({ logs, context }: Props) { )} Re-analyze +
{isPaused && ( - +
- + Logs paused {messageBuffer.length > 0 && ( diff --git a/apps/dokploy/components/dashboard/docker/mounts/show-container-mounts.tsx b/apps/dokploy/components/dashboard/docker/mounts/show-container-mounts.tsx index 0c1832fc2..2fca83fe4 100644 --- a/apps/dokploy/components/dashboard/docker/mounts/show-container-mounts.tsx +++ b/apps/dokploy/components/dashboard/docker/mounts/show-container-mounts.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, @@ -15,7 +16,6 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Badge } from "@/components/ui/badge"; import { api } from "@/utils/api"; interface Props { diff --git a/apps/dokploy/components/dashboard/docker/networks/show-container-networks.tsx b/apps/dokploy/components/dashboard/docker/networks/show-container-networks.tsx index 12a015b3a..a9555fb7d 100644 --- a/apps/dokploy/components/dashboard/docker/networks/show-container-networks.tsx +++ b/apps/dokploy/components/dashboard/docker/networks/show-container-networks.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, @@ -15,7 +16,6 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Badge } from "@/components/ui/badge"; import { api } from "@/utils/api"; interface Props { diff --git a/apps/dokploy/components/dashboard/docker/upload/upload-file-modal.tsx b/apps/dokploy/components/dashboard/docker/upload/upload-file-modal.tsx index 8838ac094..bbf640e45 100644 --- a/apps/dokploy/components/dashboard/docker/upload/upload-file-modal.tsx +++ b/apps/dokploy/components/dashboard/docker/upload/upload-file-modal.tsx @@ -26,8 +26,8 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { - uploadFileToContainerSchema, type UploadFileToContainer, + uploadFileToContainerSchema, } from "@/utils/schema"; interface Props { diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx index 8a99300ad..04d431fa1 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx @@ -1,10 +1,10 @@ +import { toast } from "sonner"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { toast } from "sonner"; interface Props { mariadbId: string; diff --git a/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx index c1e03716c..ef38fe3cb 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx @@ -1,10 +1,10 @@ +import { toast } from "sonner"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { toast } from "sonner"; interface Props { mongoId: string; diff --git a/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx index 4f91c7efc..39937badd 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx @@ -1,10 +1,10 @@ +import { toast } from "sonner"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { toast } from "sonner"; interface Props { mysqlId: string; diff --git a/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx index 30e265577..3d1b1032e 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx @@ -1,10 +1,10 @@ +import { toast } from "sonner"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; -import { toast } from "sonner"; interface Props { postgresId: string; diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index 81b60ac3a..966fe0013 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -632,7 +632,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => { control={form.control} name="enableNamespaces" render={({ field }) => { - console.log(field.value); return ( Enable Namespaces diff --git a/apps/dokploy/components/dashboard/project/add-import.tsx b/apps/dokploy/components/dashboard/project/add-import.tsx new file mode 100644 index 000000000..034710e9c --- /dev/null +++ b/apps/dokploy/components/dashboard/project/add-import.tsx @@ -0,0 +1,494 @@ +import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; +import { Code2, FileInput, Globe2, HardDrive, HelpCircle } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { slugify } from "@/lib/slug"; +import { api } from "@/utils/api"; +import { APP_NAME_MESSAGE, APP_NAME_REGEX } from "@/utils/schema"; + +const AddImportSchema = z.object({ + name: z.string().min(1, { message: "Name is required" }), + appName: z + .string() + .min(1, { message: "App name is required" }) + .regex(APP_NAME_REGEX, { message: APP_NAME_MESSAGE }), + base64: z.string().min(1, { message: "Base64 content is required" }), + serverId: z.string().optional(), +}); + +type AddImport = z.infer; + +type TemplateInfo = { + compose: string; + template: { + domains: Array<{ + serviceName: string; + port: number; + path?: string; + host?: string; + }>; + envs: string[]; + mounts: Array<{ filePath: string; content: string }>; + }; +}; + +interface Props { + environmentId: string; + projectName?: string; +} + +export const AddImport = ({ environmentId, projectName }: Props) => { + const utils = api.useUtils(); + const [visible, setVisible] = useState(false); + const [previewOpen, setPreviewOpen] = useState(false); + const [mountOpen, setMountOpen] = useState(false); + const [selectedMount, setSelectedMount] = useState<{ + filePath: string; + content: string; + } | null>(null); + const [templateInfo, setTemplateInfo] = useState(null); + + const slug = slugify(projectName); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const { data: servers } = api.server.withSSHKey.useQuery(); + const shouldShowServerDropdown = !!(servers && servers.length > 0); + + const { mutateAsync: previewTemplate, isPending: isProcessing } = + api.compose.previewTemplate.useMutation(); + const { mutateAsync: createCompose, isPending: isCreating } = + api.compose.create.useMutation(); + const { mutateAsync: importCompose, isPending: isImporting } = + api.compose.import.useMutation(); + + const form = useForm({ + defaultValues: { name: "", appName: `${slug}-`, base64: "" }, + resolver: zodResolver(AddImportSchema), + }); + + const resetAll = () => { + form.reset({ name: "", appName: `${slug}-`, base64: "" }); + setTemplateInfo(null); + setPreviewOpen(false); + setMountOpen(false); + setSelectedMount(null); + }; + + const handleOpenChange = (open: boolean) => { + if (!open) resetAll(); + setVisible(open); + }; + + const handleLoad = async (data: AddImport) => { + try { + const result = await previewTemplate({ + appName: data.appName, + base64: data.base64.trim(), + serverId: data.serverId === "dokploy" ? undefined : data.serverId, + }); + setTemplateInfo(result); + setPreviewOpen(true); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error processing template", + ); + } + }; + + const handleImport = async () => { + const data = form.getValues(); + try { + const compose = await createCompose({ + name: data.name, + appName: data.appName, + environmentId, + composeType: "docker-compose", + serverId: data.serverId === "dokploy" ? undefined : data.serverId, + }); + await importCompose({ + composeId: compose.composeId, + base64: data.base64.trim(), + }); + toast.success("Compose imported successfully"); + await utils.environment.one.invalidate({ environmentId }); + resetAll(); + setVisible(false); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Error importing compose", + ); + } + }; + + const handleCancelPreview = () => { + setPreviewOpen(false); + setTemplateInfo(null); + }; + + return ( + <> + + + e.preventDefault()} + > + + Import + + + + + Import Compose + + Paste a base64-encoded compose export to preview and import it + + + + + + ( + + Name + + { + const val = e.target.value || ""; + form.setValue( + "appName", + `${slug}-${slugify(val.trim())}`, + ); + field.onChange(val); + }} + /> + + + + )} + /> + + {shouldShowServerDropdown && ( + ( + + + + + + Select a Server {!isCloud ? "(Optional)" : ""} + + + + + + If no server is selected, the compose will be + deployed on the server where the user is logged + in. + + + + + + + + )} + /> + )} + + ( + + App Name + + + + + + )} + /> + + ( + + Configuration (Base64) + +