diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 000000000..827ccc709 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,22 @@ +name: autofix.ci + +on: + push: + branches: [canary] + pull_request: + branches: [canary] + +jobs: + format: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup biomeJs + uses: biomejs/setup-biome@v2 + + - name: Run Biome formatter + run: biome format . --write + + - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c64d0672e..015095aa6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,9 +61,9 @@ pnpm install cp apps/dokploy/.env.example apps/dokploy/.env ``` -## Development +## Requirements -Is required to have **Docker** installed on your machine. +- [Docker](/GUIDES.md#docker) ### Setup diff --git a/Dockerfile b/Dockerfile index a5bd7e5e4..ad2239b05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ WORKDIR /app # Set production ENV NODE_ENV=production -RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/* # Copy only the necessary files COPY --from=build /prod/dokploy/.next ./.next diff --git a/GUIDES.md b/GUIDES.md new file mode 100644 index 000000000..cfb7cd812 --- /dev/null +++ b/GUIDES.md @@ -0,0 +1,49 @@ +# Docker + +Here's how to install docker on different operating systems: + +## macOS + +1. Visit [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop) +2. Download the Docker Desktop installer +3. Double-click the downloaded `.dmg` file +4. Drag Docker to your Applications folder +5. Open Docker Desktop from Applications +6. Follow the onboarding tutorial if desired + +## Linux + +### Ubuntu + +```bash +# Update package index +sudo apt-get update + +# Install prerequisites +sudo apt-get install \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + lsb-release + +# Add Docker's official GPG key +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg + +# Set up stable repository +echo \ + "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# Install Docker Engine +sudo apt-get update +sudo apt-get install docker-ce docker-ce-cli containerd.io +``` + +## Windows + +1. Enable WSL2 if not already enabled +2. Visit [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop) +3. Download the installer +4. Run the installer and follow the prompts +5. Start Docker Desktop from the Start menu \ No newline at end of file diff --git a/apps/dokploy/__test__/compose/volume/volume-2.test.ts b/apps/dokploy/__test__/compose/volume/volume-2.test.ts index bf34ed494..61cba82d3 100644 --- a/apps/dokploy/__test__/compose/volume/volume-2.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-2.test.ts @@ -1006,7 +1006,7 @@ services: volumes: db-config-testhash: -`) as ComposeSpecification; +`); test("Expect to change the suffix in all the possible places (4 Try)", () => { const composeData = load(composeFileComplex) as ComposeSpecification; @@ -1115,3 +1115,60 @@ test("Expect to change the suffix in all the possible places (5 Try)", () => { expect(updatedComposeData).toEqual(expectedDockerComposeExample1); }); + +const composeFileBackrest = ` +services: + backrest: + image: garethgeorge/backrest:v1.7.3 + restart: unless-stopped + ports: + - 9898 + environment: + - BACKREST_PORT=9898 + - BACKREST_DATA=/data + - BACKREST_CONFIG=/config/config.json + - XDG_CACHE_HOME=/cache + - TZ=\${TZ} + volumes: + - backrest/data:/data + - backrest/config:/config + - backrest/cache:/cache + - /:/userdata:ro + +volumes: + backrest: + backrest-cache: +`; + +const expectedDockerComposeBackrest = load(` +services: + backrest: + image: garethgeorge/backrest:v1.7.3 + restart: unless-stopped + ports: + - 9898 + environment: + - BACKREST_PORT=9898 + - BACKREST_DATA=/data + - BACKREST_CONFIG=/config/config.json + - XDG_CACHE_HOME=/cache + - TZ=\${TZ} + volumes: + - backrest-testhash/data:/data + - backrest-testhash/config:/config + - backrest-testhash/cache:/cache + - /:/userdata:ro + +volumes: + backrest-testhash: + backrest-cache-testhash: +`) as ComposeSpecification; + +test("Should handle volume paths with subdirectories correctly", () => { + const composeData = load(composeFileBackrest) as ComposeSpecification; + const suffix = "testhash"; + + const updatedComposeData = addSuffixToAllVolumes(composeData, suffix); + + expect(updatedComposeData).toEqual(expectedDockerComposeBackrest); +}); diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts index 74c803655..7dc9e560c 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.test.ts @@ -27,6 +27,11 @@ if (typeof window === "undefined") { const baseApp: ApplicationNested = { applicationId: "", herokuVersion: "", + giteaBranch: "", + giteaBuildPath: "", + giteaId: "", + giteaOwner: "", + giteaRepository: "", cleanCache: false, watchPaths: [], applicationStatus: "done", diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts index 902e3163b..d6e87cb7a 100644 --- a/apps/dokploy/__test__/templates/config.template.test.ts +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, it } from "vitest"; +import type { Schema } from "@dokploy/server/templates"; import type { CompleteTemplate } from "@dokploy/server/templates/processors"; import { processTemplate } from "@dokploy/server/templates/processors"; -import type { Schema } from "@dokploy/server/templates"; +import { describe, expect, it } from "vitest"; describe("processTemplate", () => { // Mock schema for testing @@ -233,6 +233,49 @@ describe("processTemplate", () => { expect(base64Value.length).toBeGreaterThanOrEqual(42); expect(base64Value.length).toBeLessThanOrEqual(44); }); + + it("should handle boolean values in env vars when provided as an array", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: [ + "ENABLE_USER_SIGN_UP=false", + "DEBUG_MODE=true", + "SOME_NUMBER=42", + ], + mounts: [], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(3); + expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false"); + expect(result.envs).toContain("DEBUG_MODE=true"); + expect(result.envs).toContain("SOME_NUMBER=42"); + }); + + it("should handle boolean values in env vars when provided as an object", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: { + ENABLE_USER_SIGN_UP: false, + DEBUG_MODE: true, + SOME_NUMBER: 42, + }, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(3); + expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false"); + expect(result.envs).toContain("DEBUG_MODE=true"); + expect(result.envs).toContain("SOME_NUMBER=42"); + }); }); describe("mounts processing", () => { diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 74b0e265b..d8a14ab42 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -7,6 +7,11 @@ import { expect, test } from "vitest"; const baseApp: ApplicationNested = { applicationId: "", herokuVersion: "", + giteaRepository: "", + giteaOwner: "", + giteaBranch: "", + giteaBuildPath: "", + giteaId: "", cleanCache: false, applicationStatus: "done", appName: "", diff --git a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx index 2a3f2f43a..0e848fece 100644 --- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; import { @@ -32,7 +33,6 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -import { AlertBlock } from "@/components/shared/alert-block"; const ImportSchema = z.object({ base64: z.string(), diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index 8b1fa7e1c..8da85a879 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -41,8 +41,8 @@ import { toast } from "sonner"; import { domain } from "@/server/db/validations/domain"; import { zodResolver } from "@hookform/resolvers/zod"; import { Dices } from "lucide-react"; -import type z from "zod"; import Link from "next/link"; +import type z from "zod"; type Domain = z.infer; diff --git a/apps/dokploy/components/dashboard/application/environment/show.tsx b/apps/dokploy/components/dashboard/application/environment/show.tsx index b574ce092..6f504959c 100644 --- a/apps/dokploy/components/dashboard/application/environment/show.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show.tsx @@ -4,10 +4,10 @@ import { Form } from "@/components/ui/form"; import { Secrets } from "@/components/ui/secrets"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -import { useEffect } from "react"; const addEnvironmentSchema = z.object({ env: z.string(), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx index ca1bf823f..b506fbac5 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx @@ -1,4 +1,6 @@ +import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, @@ -39,13 +41,11 @@ import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { CheckIcon, ChevronsUpDown, 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 { Badge } from "@/components/ui/badge"; -import { BitbucketIcon } from "@/components/icons/data-tools-icons"; -import Link from "next/link"; const BitbucketProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx index 6f51af3d6..a1f3367dd 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx @@ -115,7 +115,11 @@ export const SaveDockerProvider = ({ applicationId }: Props) => { Username - + @@ -130,7 +134,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => { Password - + diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx index 3d6f6a388..a7020c59d 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx @@ -26,15 +26,15 @@ import { import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { KeyRoundIcon, LockIcon, X } from "lucide-react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; +import { GitIcon } from "@/components/icons/data-tools-icons"; +import { Badge } from "@/components/ui/badge"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -import { Badge } from "@/components/ui/badge"; -import { GitIcon } from "@/components/icons/data-tools-icons"; const GitProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), 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 new file mode 100644 index 000000000..0ad889452 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx @@ -0,0 +1,515 @@ +import { GiteaIcon } from "@/components/icons/data-tools-icons"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + 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"; +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"; + +interface GiteaRepository { + name: string; + url: string; + id: number; + owner: { + username: string; + }; +} + +interface GiteaBranch { + name: string; + commit: { + id: string; + }; +} + +const GiteaProviderSchema = z.object({ + buildPath: z.string().min(1, "Path is required").default("/"), + repository: z + .object({ + repo: z.string().min(1, "Repo is required"), + owner: z.string().min(1, "Owner is required"), + }) + .required(), + branch: z.string().min(1, "Branch is required"), + giteaId: z.string().min(1, "Gitea Provider is required"), + watchPaths: z.array(z.string()).default([]), +}); + +type GiteaProvider = z.infer; + +interface Props { + applicationId: string; +} + +export const SaveGiteaProvider = ({ applicationId }: Props) => { + const { data: giteaProviders } = api.gitea.giteaProviders.useQuery(); + const { data, refetch } = api.application.one.useQuery({ applicationId }); + + const { mutateAsync, isLoading: isSavingGiteaProvider } = + api.application.saveGiteaProvider.useMutation(); + + const form = useForm({ + defaultValues: { + buildPath: "/", + repository: { + owner: "", + repo: "", + }, + giteaId: "", + branch: "", + watchPaths: [], + }, + resolver: zodResolver(GiteaProviderSchema), + }); + + const repository = form.watch("repository"); + const giteaId = form.watch("giteaId"); + + const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery( + { giteaId }, + { + enabled: !!giteaId, + }, + ); + + const { + data: repositories, + isLoading: isLoadingRepositories, + error, + } = api.gitea.getGiteaRepositories.useQuery( + { + giteaId, + }, + { + enabled: !!giteaId, + }, + ); + + const { + data: branches, + fetchStatus, + status, + } = api.gitea.getGiteaBranches.useQuery( + { + owner: repository?.owner, + repositoryName: repository?.repo, + giteaId: giteaId, + }, + { + enabled: !!repository?.owner && !!repository?.repo && !!giteaId, + }, + ); + + useEffect(() => { + if (data) { + form.reset({ + branch: data.giteaBranch || "", + repository: { + repo: data.giteaRepository || "", + owner: data.giteaOwner || "", + }, + buildPath: data.giteaBuildPath || "/", + giteaId: data.giteaId || "", + watchPaths: data.watchPaths || [], + }); + } + }, [form.reset, data, form]); + + const onSubmit = async (data: GiteaProvider) => { + await mutateAsync({ + giteaBranch: data.branch, + giteaRepository: data.repository.repo, + giteaOwner: data.repository.owner, + giteaBuildPath: data.buildPath, + giteaId: data.giteaId, + applicationId, + watchPaths: data.watchPaths, + }) + .then(async () => { + toast.success("Service Provider Saved"); + await refetch(); + }) + .catch(() => { + toast.error("Error saving the Gitea provider"); + }); + }; + + return ( +
+
+ + {error && {error?.message}} +
+ ( + + Gitea Account + + + + )} + /> + + ( + +
+ Repository + {field.value.owner && field.value.repo && ( + + + View Repository + + )} +
+ + + + + + + + + + + {isLoadingRepositories && ( + + Loading Repositories.... + + )} + No repositories found. + + + {repositories && repositories.length === 0 && ( + + No repositories found. + + )} + {repositories?.map((repo: GiteaRepository) => { + return ( + { + form.setValue("repository", { + owner: repo.owner.username as string, + repo: repo.name, + }); + form.setValue("branch", ""); + }} + > + + {repo.name} + + {repo.owner.username} + + + + + ); + })} + + + + + + {form.formState.errors.repository && ( +

+ Repository is required +

+ )} +
+ )} + /> + ( + + Branch + + + + + + + + + + {status === "loading" && fetchStatus === "fetching" && ( + + Loading Branches.... + + )} + {!repository?.owner && ( + + Select a repository + + )} + + No branch found. + + + {branches?.map((branch: GiteaBranch) => ( + { + form.setValue("branch", branch.name); + }} + > + {branch.name} + + + ))} + + + + + + + + + )} + /> + ( + + Build Path + + + + + + + )} + /> + ( + +
+ Watch Paths + + + + + + +

+ Add paths to watch for changes. When files in these + paths change, a new deployment will be triggered. +

+
+
+
+
+
+ {field.value?.map((path: string, index: number) => ( + + {path} + { + const newPaths = [...field.value]; + newPaths.splice(index, 1); + field.onChange(newPaths); + }} + /> + + ))} +
+
+ + { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const path = input.value.trim(); + if (path) { + field.onChange([...field.value, path]); + input.value = ""; + } + } + }} + /> + + +
+ +
+ )} + /> +
+
+ +
+
+ +
+ ); +}; 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 202c7f880..2a267a185 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 @@ -1,3 +1,5 @@ +import { GithubIcon } from "@/components/icons/data-tools-icons"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, @@ -34,17 +36,15 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; 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 Link from "next/link"; -import { GithubIcon } from "@/components/icons/data-tools-icons"; const GithubProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), 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 c0c90a016..0f8bb849e 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 @@ -1,4 +1,6 @@ +import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, @@ -35,17 +37,15 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; 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 Link from "next/link"; -import { GitlabIcon } from "@/components/icons/data-tools-icons"; const GitlabProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx index b00a34953..3f8854888 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx @@ -1,10 +1,12 @@ import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider"; import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider"; +import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider"; import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider"; import { BitbucketIcon, DockerIcon, GitIcon, + GiteaIcon, GithubIcon, GitlabIcon, } from "@/components/icons/data-tools-icons"; @@ -18,7 +20,14 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider"; import { SaveDragNDrop } from "./save-drag-n-drop"; import { SaveGitlabProvider } from "./save-gitlab-provider"; -type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket"; +type TabState = + | "github" + | "docker" + | "git" + | "drop" + | "gitlab" + | "bitbucket" + | "gitea"; interface Props { applicationId: string; @@ -29,6 +38,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => { const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery(); const { data: bitbucketProviders } = api.bitbucket.bitbucketProviders.useQuery(); + const { data: giteaProviders } = api.gitea.giteaProviders.useQuery(); const { data: application } = api.application.one.useQuery({ applicationId }); const [tab, setSab] = useState(application?.sourceType || "github"); @@ -78,6 +88,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => { Bitbucket + + + Gitea + { )} + + {giteaProviders && giteaProviders?.length > 0 ? ( + + ) : ( +
+ + + To deploy using Gitea, you need to configure your account + first. Please, go to{" "} + + Settings + {" "} + to do so. + +
+ )} +
diff --git a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx index 975ce1ffe..6089c99ff 100644 --- a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx @@ -41,8 +41,8 @@ import { import { domainCompose } from "@/server/db/validations/domain"; import { zodResolver } from "@hookform/resolvers/zod"; import { DatabaseZap, Dices, RefreshCw } from "lucide-react"; -import type z from "zod"; import Link from "next/link"; +import type z from "zod"; type Domain = z.infer; 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 6dc99b267..ff329a0af 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 @@ -1,4 +1,6 @@ +import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, @@ -39,13 +41,11 @@ import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { CheckIcon, ChevronsUpDown, 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 { Badge } from "@/components/ui/badge"; -import { BitbucketIcon } from "@/components/icons/data-tools-icons"; -import Link from "next/link"; const BitbucketProviderSchema = z.object({ composePath: z.string().min(1), 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 ebe998923..68891e456 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 @@ -1,3 +1,4 @@ +import { GitIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -27,13 +28,12 @@ import { import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { KeyRoundIcon, LockIcon, X } from "lucide-react"; +import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -import { GitIcon } from "@/components/icons/data-tools-icons"; -import Link from "next/link"; const GitProviderSchema = z.object({ composePath: z.string().min(1), 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 new file mode 100644 index 000000000..201f9da2e --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx @@ -0,0 +1,483 @@ +import { GiteaIcon } from "@/components/icons/data-tools-icons"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + 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 type { Repository } from "@/utils/gitea-utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { CheckIcon, ChevronsUpDown, 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"; + +const GiteaProviderSchema = z.object({ + composePath: z.string().min(1), + repository: z + .object({ + repo: z.string().min(1, "Repo is required"), + owner: z.string().min(1, "Owner is required"), + }) + .required(), + branch: z.string().min(1, "Branch is required"), + giteaId: z.string().min(1, "Gitea Provider is required"), + watchPaths: z.array(z.string()).optional(), +}); + +type GiteaProvider = z.infer; + +interface Props { + composeId: string; +} + +export const SaveGiteaProviderCompose = ({ composeId }: Props) => { + const { data: giteaProviders } = api.gitea.giteaProviders.useQuery(); + const { data, refetch } = api.compose.one.useQuery({ composeId }); + const { mutateAsync, isLoading: isSavingGiteaProvider } = + api.compose.update.useMutation(); + + const form = useForm({ + defaultValues: { + composePath: "./docker-compose.yml", + repository: { + owner: "", + repo: "", + }, + giteaId: "", + branch: "", + watchPaths: [], + }, + resolver: zodResolver(GiteaProviderSchema), + }); + + const repository = form.watch("repository"); + const giteaId = form.watch("giteaId"); + + const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery( + { giteaId }, + { + enabled: !!giteaId, + }, + ); + + const { + data: repositories, + isLoading: isLoadingRepositories, + error, + } = api.gitea.getGiteaRepositories.useQuery( + { + giteaId, + }, + { + enabled: !!giteaId, + }, + ); + + const { + data: branches, + fetchStatus, + status, + } = api.gitea.getGiteaBranches.useQuery( + { + owner: repository?.owner, + repositoryName: repository?.repo, + giteaId: giteaId, + }, + { + enabled: !!repository?.owner && !!repository?.repo && !!giteaId, + }, + ); + + useEffect(() => { + if (data) { + form.reset({ + branch: data.giteaBranch || "", + repository: { + repo: data.giteaRepository || "", + owner: data.giteaOwner || "", + }, + composePath: data.composePath || "./docker-compose.yml", + giteaId: data.giteaId || "", + watchPaths: data.watchPaths || [], + }); + } + }, [form.reset, data, form]); + + const onSubmit = async (data: GiteaProvider) => { + await mutateAsync({ + giteaBranch: data.branch, + giteaRepository: data.repository.repo, + giteaOwner: data.repository.owner, + composePath: data.composePath, + giteaId: data.giteaId, + composeId, + sourceType: "gitea", + composeStatus: "idle", + watchPaths: data.watchPaths, + } as any) + .then(async () => { + toast.success("Service Provider Saved"); + await refetch(); + }) + .catch(() => { + toast.error("Error saving the Gitea provider"); + }); + }; + + return ( +
+
+ + {error && {error?.message}} + +
+ ( + + Gitea Account + + + + )} + /> + + ( + +
+ Repository + {field.value.owner && field.value.repo && ( + + + View Repository + + )} +
+ + + + + + + + + + {isLoadingRepositories && ( + + Loading Repositories.... + + )} + No repositories found. + + + {repositories?.map((repo) => ( + { + form.setValue("repository", { + owner: repo.owner.username, + repo: repo.name, + }); + form.setValue("branch", ""); + }} + > + + {repo.name} + + {repo.owner.username} + + + + + ))} + + + + + + {form.formState.errors.repository && ( +

+ Repository is required +

+ )} +
+ )} + /> + + ( + + Branch + + + + + + + + + + No branches found. + + + {branches?.map((branch) => ( + + form.setValue("branch", branch.name) + } + > + + {branch.name} + + + + ))} + + + + + + {form.formState.errors.branch && ( +

+ Branch is required +

+ )} +
+ )} + /> + + ( + + Compose Path + + + + + + )} + /> + + ( + +
+ Watch Paths + + + +
+ ? +
+
+ +

+ Add paths to watch for changes. When files in these + paths change, a new deployment will be triggered. +

+
+
+
+
+
+ {field.value?.map((path, index) => ( + + {path} + { + const newPaths = [...(field.value || [])]; + newPaths.splice(index, 1); + form.setValue("watchPaths", newPaths); + }} + /> + + ))} +
+ +
+ { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const value = input.value.trim(); + if (value) { + const newPaths = [...(field.value || []), value]; + form.setValue("watchPaths", newPaths); + input.value = ""; + } + } + }} + /> + +
+
+ +
+ )} + /> +
+ +
+ +
+
+ +
+ ); +}; 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 b58347dc3..4f4c1d5ad 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 { GithubIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -39,12 +40,11 @@ import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { CheckIcon, ChevronsUpDown, 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 { GithubIcon } from "@/components/icons/data-tools-icons"; -import Link from "next/link"; const GithubProviderSchema = z.object({ composePath: z.string().min(1), 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 693fea718..c191248ea 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 @@ -1,4 +1,6 @@ +import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, @@ -39,13 +41,11 @@ import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { CheckIcon, ChevronsUpDown, 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 { Badge } from "@/components/ui/badge"; -import { GitlabIcon } from "@/components/icons/data-tools-icons"; -import Link from "next/link"; const GitlabProviderSchema = z.object({ composePath: z.string().min(1), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx index 347c134e3..2ac879e87 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/show.tsx @@ -1,6 +1,7 @@ import { BitbucketIcon, GitIcon, + GiteaIcon, GithubIcon, GitlabIcon, } from "@/components/icons/data-tools-icons"; @@ -14,10 +15,11 @@ import { ComposeFileEditor } from "../compose-file-editor"; import { ShowConvertedCompose } from "../show-converted-compose"; import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose"; import { SaveGitProviderCompose } from "./save-git-provider-compose"; +import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose"; import { SaveGithubProviderCompose } from "./save-github-provider-compose"; import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose"; -type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket"; +type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea"; interface Props { composeId: string; } @@ -27,9 +29,11 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => { const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery(); const { data: bitbucketProviders } = api.bitbucket.bitbucketProviders.useQuery(); + const { data: giteaProviders } = api.gitea.giteaProviders.useQuery(); const { data: compose } = api.compose.one.useQuery({ composeId }); const [tab, setSab] = useState(compose?.sourceType || "github"); + return ( @@ -54,21 +58,21 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => { setSab(e as TabState); }} > -
- +
+ - Github + GitHub - Gitlab + GitLab { Bitbucket - + + Gitea + { value="raw" className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border" > - + Raw
+ {githubProviders && githubProviders?.length > 0 ? ( @@ -154,6 +164,26 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
)} + + {giteaProviders && giteaProviders?.length > 0 ? ( + + ) : ( +
+ + + To deploy using Gitea, you need to configure your account + first. Please, go to{" "} + + Settings + {" "} + to do so. + +
+ )} +
diff --git a/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx b/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx index 1eb13cadf..8ee9c786b 100644 --- a/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx +++ b/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx @@ -147,7 +147,9 @@ export const IsolatedDeployment = ({ composeId }: Props) => { render={({ field }) => (
- Enable Isolated Deployment ({data?.appName}) + + Enable Isolated Deployment ({data?.appName}) + Enable isolated deployment to the compose file. diff --git a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx b/apps/dokploy/components/dashboard/database/backups/add-backup.tsx index 219e42184..0fa568a90 100644 --- a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/add-backup.tsx @@ -61,7 +61,7 @@ type AddPostgresBackup = z.infer; interface Props { databaseId: string; - databaseType: "postgres" | "mariadb" | "mysql" | "mongo"; + databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server"; refetch: () => void; } @@ -85,7 +85,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { useEffect(() => { form.reset({ - database: "", + database: databaseType === "web-server" ? "dokploy" : "", destinationId: "", enabled: true, prefix: "/", @@ -112,7 +112,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { ? { mongoId: databaseId, } - : undefined; + : databaseType === "web-server" + ? { + userId: databaseId, + } + : undefined; await createBackup({ destinationId: data.destinationId, @@ -236,7 +240,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { Database - + @@ -286,16 +294,21 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { Keep the latest - + - Optional. If provided, only keeps the latest N backups in the cloud. + Optional. If provided, only keeps the latest N backups + in the cloud. ); }} - /> + /> ; - serverId: string | null; + databaseType: Exclude | "web-server"; + serverId?: string | null; } const RestoreBackupSchema = z.object({ @@ -91,7 +91,7 @@ export const RestoreBackup = ({ defaultValues: { destinationId: "", backupFile: "", - databaseName: "", + databaseName: databaseType === "web-server" ? "dokploy" : "", }, resolver: zodResolver(RestoreBackupSchema), }); @@ -340,7 +340,11 @@ export const RestoreBackup = ({ Database Name - + diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index cdfe614c8..1c2b527bf 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -14,18 +14,18 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { DatabaseBackup, Play, Trash2 } from "lucide-react"; +import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react"; import Link from "next/link"; +import { useState } from "react"; import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; import { AddBackup } from "./add-backup"; -import { UpdateBackup } from "./update-backup"; import { RestoreBackup } from "./restore-backup"; -import { useState } from "react"; +import { UpdateBackup } from "./update-backup"; interface Props { id: string; - type: Exclude; + type: Exclude | "web-server"; } export const ShowBackups = ({ id, type }: Props) => { const [activeManualBackup, setActiveManualBackup] = useState< @@ -38,6 +38,7 @@ export const ShowBackups = ({ id, type }: Props) => { mariadb: () => api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + "web-server": () => api.user.getBackups.useQuery(), }; const { data } = api.destination.all.useQuery(); const { data: postgres, refetch } = queryMap[type] @@ -49,6 +50,7 @@ export const ShowBackups = ({ id, type }: Props) => { mysql: () => api.backup.manualBackupMySql.useMutation(), mariadb: () => api.backup.manualBackupMariadb.useMutation(), mongo: () => api.backup.manualBackupMongo.useMutation(), + "web-server": () => api.backup.manualBackupWebServer.useMutation(), }; const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[ @@ -64,7 +66,10 @@ export const ShowBackups = ({ id, type }: Props) => {
- Backups + + + Backups + Add backups to your database to save the data to a different provider. @@ -73,11 +78,17 @@ export const ShowBackups = ({ id, type }: Props) => { {postgres && postgres?.backups?.length > 0 && (
- + {type !== "web-server" && ( + + )}
)} @@ -115,7 +126,9 @@ export const ShowBackups = ({ id, type }: Props) => {
diff --git a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx index a98ad84af..2cf7b7a5f 100644 --- a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx @@ -92,7 +92,9 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { enabled: backup.enabled || false, prefix: backup.prefix, schedule: backup.schedule, - keepLatestCount: backup.keepLatestCount ? Number(backup.keepLatestCount) : undefined, + keepLatestCount: backup.keepLatestCount + ? Number(backup.keepLatestCount) + : undefined, }); } }, [form, form.reset, backup]); @@ -274,10 +276,15 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { Keep the latest - + - Optional. If provided, only keeps the latest N backups in the cloud. + Optional. If provided, only keeps the latest N backups + in the cloud. diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx index 9f5b63c31..c00af42be 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx @@ -27,145 +27,149 @@ import { toast } from "sonner"; import { z } from "zod"; const DockerProviderSchema = z.object({ - externalPort: z.preprocess((a) => { - if (a !== null) { - const parsed = Number.parseInt(z.string().parse(a), 10); - return Number.isNaN(parsed) ? null : parsed; - } - return null; - }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()), + externalPort: z.preprocess((a) => { + if (a !== null) { + const parsed = Number.parseInt(z.string().parse(a), 10); + return Number.isNaN(parsed) ? null : parsed; + } + return null; + }, z + .number() + .gte(0, "Range must be 0 - 65535") + .lte(65535, "Range must be 0 - 65535") + .nullable()), }); type DockerProvider = z.infer; interface Props { - mariadbId: string; + mariadbId: string; } export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => { - const { data: ip } = api.settings.getIp.useQuery(); - const { data, refetch } = api.mariadb.one.useQuery({ mariadbId }); - const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation(); - const [connectionUrl, setConnectionUrl] = useState(""); - const getIp = data?.server?.ipAddress || ip; - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(DockerProviderSchema), - }); + const { data: ip } = api.settings.getIp.useQuery(); + const { data, refetch } = api.mariadb.one.useQuery({ mariadbId }); + const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation(); + const [connectionUrl, setConnectionUrl] = useState(""); + const getIp = data?.server?.ipAddress || ip; + const form = useForm({ + defaultValues: {}, + resolver: zodResolver(DockerProviderSchema), + }); - useEffect(() => { - if (data?.externalPort) { - form.reset({ - externalPort: data.externalPort, - }); - } - }, [form.reset, data, form]); + useEffect(() => { + if (data?.externalPort) { + form.reset({ + externalPort: data.externalPort, + }); + } + }, [form.reset, data, form]); - const onSubmit = async (values: DockerProvider) => { - await mutateAsync({ - externalPort: values.externalPort, - mariadbId, - }) - .then(async () => { - toast.success("External Port updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error saving the external port"); - }); - }; + const onSubmit = async (values: DockerProvider) => { + await mutateAsync({ + externalPort: values.externalPort, + mariadbId, + }) + .then(async () => { + toast.success("External Port updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error saving the external port"); + }); + }; - useEffect(() => { - const buildConnectionUrl = () => { - const port = form.watch("externalPort") || data?.externalPort; + useEffect(() => { + const buildConnectionUrl = () => { + const port = form.watch("externalPort") || data?.externalPort; - return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; - }; + return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; + }; - setConnectionUrl(buildConnectionUrl()); - }, [ - data?.appName, - data?.externalPort, - data?.databasePassword, - form, - data?.databaseName, - data?.databaseUser, - getIp, - ]); - return ( - <> -
- - - External Credentials - - In order to make the database reachable trought internet is - required to set a port, make sure the port is not used by another - application or database - - - - {!getIp && ( - - You need to set an IP address in your{" "} - - {data?.serverId - ? "Remote Servers -> Server -> Edit Server -> Update IP Address" - : "Web Server -> Server -> Update Server IP"} - {" "} - to fix the database url connection. - - )} -
- -
-
- { - return ( - - External Port (Internet) - - - - - - ); - }} - /> -
-
- {!!data?.externalPort && ( -
-
- {/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */} - - -
-
- )} + setConnectionUrl(buildConnectionUrl()); + }, [ + data?.appName, + data?.externalPort, + data?.databasePassword, + form, + data?.databaseName, + data?.databaseUser, + getIp, + ]); + return ( + <> +
+ + + External Credentials + + In order to make the database reachable trought internet is + required to set a port, make sure the port is not used by another + application or database + + + + {!getIp && ( + + You need to set an IP address in your{" "} + + {data?.serverId + ? "Remote Servers -> Server -> Edit Server -> Update IP Address" + : "Web Server -> Server -> Update Server IP"} + {" "} + to fix the database url connection. + + )} + + +
+
+ { + return ( + + External Port (Internet) + + + + + + ); + }} + /> +
+
+ {!!data?.externalPort && ( +
+
+ {/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */} + + +
+
+ )} -
- -
- - -
-
-
- - ); +
+ +
+ + +
+
+
+ + ); }; diff --git a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx index b5ed9f863..75772bfdf 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-external-mongo-credentials.tsx @@ -27,144 +27,148 @@ import { toast } from "sonner"; import { z } from "zod"; const DockerProviderSchema = z.object({ - externalPort: z.preprocess((a) => { - if (a !== null) { - const parsed = Number.parseInt(z.string().parse(a), 10); - return Number.isNaN(parsed) ? null : parsed; - } - return null; - }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()), + externalPort: z.preprocess((a) => { + if (a !== null) { + const parsed = Number.parseInt(z.string().parse(a), 10); + return Number.isNaN(parsed) ? null : parsed; + } + return null; + }, z + .number() + .gte(0, "Range must be 0 - 65535") + .lte(65535, "Range must be 0 - 65535") + .nullable()), }); type DockerProvider = z.infer; interface Props { - mongoId: string; + mongoId: string; } export const ShowExternalMongoCredentials = ({ mongoId }: Props) => { - const { data: ip } = api.settings.getIp.useQuery(); - const { data, refetch } = api.mongo.one.useQuery({ mongoId }); - const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation(); - const [connectionUrl, setConnectionUrl] = useState(""); - const getIp = data?.server?.ipAddress || ip; - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(DockerProviderSchema), - }); + const { data: ip } = api.settings.getIp.useQuery(); + const { data, refetch } = api.mongo.one.useQuery({ mongoId }); + const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation(); + const [connectionUrl, setConnectionUrl] = useState(""); + const getIp = data?.server?.ipAddress || ip; + const form = useForm({ + defaultValues: {}, + resolver: zodResolver(DockerProviderSchema), + }); - useEffect(() => { - if (data?.externalPort) { - form.reset({ - externalPort: data.externalPort, - }); - } - }, [form.reset, data, form]); + useEffect(() => { + if (data?.externalPort) { + form.reset({ + externalPort: data.externalPort, + }); + } + }, [form.reset, data, form]); - const onSubmit = async (values: DockerProvider) => { - await mutateAsync({ - externalPort: values.externalPort, - mongoId, - }) - .then(async () => { - toast.success("External Port updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error saving the external port"); - }); - }; + const onSubmit = async (values: DockerProvider) => { + await mutateAsync({ + externalPort: values.externalPort, + mongoId, + }) + .then(async () => { + toast.success("External Port updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error saving the external port"); + }); + }; - useEffect(() => { - const buildConnectionUrl = () => { - const port = form.watch("externalPort") || data?.externalPort; + useEffect(() => { + const buildConnectionUrl = () => { + const port = form.watch("externalPort") || data?.externalPort; - return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`; - }; + return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`; + }; - setConnectionUrl(buildConnectionUrl()); - }, [ - data?.appName, - data?.externalPort, - data?.databasePassword, - form, - data?.databaseUser, - getIp, - ]); + setConnectionUrl(buildConnectionUrl()); + }, [ + data?.appName, + data?.externalPort, + data?.databasePassword, + form, + data?.databaseUser, + getIp, + ]); - return ( - <> -
- - - External Credentials - - In order to make the database reachable trought internet is - required to set a port, make sure the port is not used by another - application or database - - - - {!getIp && ( - - You need to set an IP address in your{" "} - - {data?.serverId - ? "Remote Servers -> Server -> Edit Server -> Update IP Address" - : "Web Server -> Server -> Update Server IP"} - {" "} - to fix the database url connection. - - )} -
- -
-
- { - return ( - - External Port (Internet) - - - - - - ); - }} - /> -
-
- {!!data?.externalPort && ( -
-
- - -
-
- )} + return ( + <> +
+ + + External Credentials + + In order to make the database reachable trought internet is + required to set a port, make sure the port is not used by another + application or database + + + + {!getIp && ( + + You need to set an IP address in your{" "} + + {data?.serverId + ? "Remote Servers -> Server -> Edit Server -> Update IP Address" + : "Web Server -> Server -> Update Server IP"} + {" "} + to fix the database url connection. + + )} + + +
+
+ { + return ( + + External Port (Internet) + + + + + + ); + }} + /> +
+
+ {!!data?.externalPort && ( +
+
+ + +
+
+ )} -
- -
- - -
-
-
- - ); +
+ +
+ + +
+
+
+ + ); }; diff --git a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx index 2c8ed5f5b..73f99b7d0 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-external-mysql-credentials.tsx @@ -27,144 +27,148 @@ import { toast } from "sonner"; import { z } from "zod"; const DockerProviderSchema = z.object({ - externalPort: z.preprocess((a) => { - if (a !== null) { - const parsed = Number.parseInt(z.string().parse(a), 10); - return Number.isNaN(parsed) ? null : parsed; - } - return null; - }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()), + externalPort: z.preprocess((a) => { + if (a !== null) { + const parsed = Number.parseInt(z.string().parse(a), 10); + return Number.isNaN(parsed) ? null : parsed; + } + return null; + }, z + .number() + .gte(0, "Range must be 0 - 65535") + .lte(65535, "Range must be 0 - 65535") + .nullable()), }); type DockerProvider = z.infer; interface Props { - mysqlId: string; + mysqlId: string; } export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => { - const { data: ip } = api.settings.getIp.useQuery(); - const { data, refetch } = api.mysql.one.useQuery({ mysqlId }); - const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation(); - const [connectionUrl, setConnectionUrl] = useState(""); - const getIp = data?.server?.ipAddress || ip; - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(DockerProviderSchema), - }); + const { data: ip } = api.settings.getIp.useQuery(); + const { data, refetch } = api.mysql.one.useQuery({ mysqlId }); + const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation(); + const [connectionUrl, setConnectionUrl] = useState(""); + const getIp = data?.server?.ipAddress || ip; + const form = useForm({ + defaultValues: {}, + resolver: zodResolver(DockerProviderSchema), + }); - useEffect(() => { - if (data?.externalPort) { - form.reset({ - externalPort: data.externalPort, - }); - } - }, [form.reset, data, form]); + useEffect(() => { + if (data?.externalPort) { + form.reset({ + externalPort: data.externalPort, + }); + } + }, [form.reset, data, form]); - const onSubmit = async (values: DockerProvider) => { - await mutateAsync({ - externalPort: values.externalPort, - mysqlId, - }) - .then(async () => { - toast.success("External Port updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error saving the external port"); - }); - }; + const onSubmit = async (values: DockerProvider) => { + await mutateAsync({ + externalPort: values.externalPort, + mysqlId, + }) + .then(async () => { + toast.success("External Port updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error saving the external port"); + }); + }; - useEffect(() => { - const buildConnectionUrl = () => { - const port = form.watch("externalPort") || data?.externalPort; + useEffect(() => { + const buildConnectionUrl = () => { + const port = form.watch("externalPort") || data?.externalPort; - return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; - }; + return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; + }; - setConnectionUrl(buildConnectionUrl()); - }, [ - data?.appName, - data?.externalPort, - data?.databasePassword, - data?.databaseName, - data?.databaseUser, - form, - getIp, - ]); - return ( - <> -
- - - External Credentials - - In order to make the database reachable trought internet is - required to set a port, make sure the port is not used by another - application or database - - - - {!getIp && ( - - You need to set an IP address in your{" "} - - {data?.serverId - ? "Remote Servers -> Server -> Edit Server -> Update IP Address" - : "Web Server -> Server -> Update Server IP"} - {" "} - to fix the database url connection. - - )} -
- -
-
- { - return ( - - External Port (Internet) - - - - - - ); - }} - /> -
-
- {!!data?.externalPort && ( -
-
- - -
-
- )} + setConnectionUrl(buildConnectionUrl()); + }, [ + data?.appName, + data?.externalPort, + data?.databasePassword, + data?.databaseName, + data?.databaseUser, + form, + getIp, + ]); + return ( + <> +
+ + + External Credentials + + In order to make the database reachable trought internet is + required to set a port, make sure the port is not used by another + application or database + + + + {!getIp && ( + + You need to set an IP address in your{" "} + + {data?.serverId + ? "Remote Servers -> Server -> Edit Server -> Update IP Address" + : "Web Server -> Server -> Update Server IP"} + {" "} + to fix the database url connection. + + )} + + +
+
+ { + return ( + + External Port (Internet) + + + + + + ); + }} + /> +
+
+ {!!data?.externalPort && ( +
+
+ + +
+
+ )} -
- -
- - -
-
-
- - ); +
+ +
+ + +
+
+
+ + ); }; diff --git a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx index 0c87a7bcd..444fa0cee 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-external-postgres-credentials.tsx @@ -27,146 +27,150 @@ import { toast } from "sonner"; import { z } from "zod"; const DockerProviderSchema = z.object({ - externalPort: z.preprocess((a) => { - if (a !== null) { - const parsed = Number.parseInt(z.string().parse(a), 10); - return Number.isNaN(parsed) ? null : parsed; - } - return null; - }, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()), + externalPort: z.preprocess((a) => { + if (a !== null) { + const parsed = Number.parseInt(z.string().parse(a), 10); + return Number.isNaN(parsed) ? null : parsed; + } + return null; + }, z + .number() + .gte(0, "Range must be 0 - 65535") + .lte(65535, "Range must be 0 - 65535") + .nullable()), }); type DockerProvider = z.infer; interface Props { - postgresId: string; + postgresId: string; } export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => { - const { data: ip } = api.settings.getIp.useQuery(); - const { data, refetch } = api.postgres.one.useQuery({ postgresId }); - const { mutateAsync, isLoading } = - api.postgres.saveExternalPort.useMutation(); - const getIp = data?.server?.ipAddress || ip; - const [connectionUrl, setConnectionUrl] = useState(""); + const { data: ip } = api.settings.getIp.useQuery(); + const { data, refetch } = api.postgres.one.useQuery({ postgresId }); + const { mutateAsync, isLoading } = + api.postgres.saveExternalPort.useMutation(); + const getIp = data?.server?.ipAddress || ip; + const [connectionUrl, setConnectionUrl] = useState(""); - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(DockerProviderSchema), - }); + const form = useForm({ + defaultValues: {}, + resolver: zodResolver(DockerProviderSchema), + }); - useEffect(() => { - if (data?.externalPort) { - form.reset({ - externalPort: data.externalPort, - }); - } - }, [form.reset, data, form]); + useEffect(() => { + if (data?.externalPort) { + form.reset({ + externalPort: data.externalPort, + }); + } + }, [form.reset, data, form]); - const onSubmit = async (values: DockerProvider) => { - await mutateAsync({ - externalPort: values.externalPort, - postgresId, - }) - .then(async () => { - toast.success("External Port updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error saving the external port"); - }); - }; + const onSubmit = async (values: DockerProvider) => { + await mutateAsync({ + externalPort: values.externalPort, + postgresId, + }) + .then(async () => { + toast.success("External Port updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error saving the external port"); + }); + }; - useEffect(() => { - const buildConnectionUrl = () => { - const port = form.watch("externalPort") || data?.externalPort; + useEffect(() => { + const buildConnectionUrl = () => { + const port = form.watch("externalPort") || data?.externalPort; - return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; - }; + return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`; + }; - setConnectionUrl(buildConnectionUrl()); - }, [ - data?.appName, - data?.externalPort, - data?.databasePassword, - form, - data?.databaseName, - getIp, - ]); + setConnectionUrl(buildConnectionUrl()); + }, [ + data?.appName, + data?.externalPort, + data?.databasePassword, + form, + data?.databaseName, + getIp, + ]); - return ( - <> -
- - - External Credentials - - In order to make the database reachable trought internet is - required to set a port, make sure the port is not used by another - application or database - - - - {!getIp && ( - - You need to set an IP address in your{" "} - - {data?.serverId - ? "Remote Servers -> Server -> Edit Server -> Update IP Address" - : "Web Server -> Server -> Update Server IP"} - {" "} - to fix the database url connection. - - )} -
- -
-
- { - return ( - - External Port (Internet) - - - - - - ); - }} - /> -
-
- {!!data?.externalPort && ( -
-
- - -
-
- )} + return ( + <> +
+ + + External Credentials + + In order to make the database reachable trought internet is + required to set a port, make sure the port is not used by another + application or database + + + + {!getIp && ( + + You need to set an IP address in your{" "} + + {data?.serverId + ? "Remote Servers -> Server -> Edit Server -> Update IP Address" + : "Web Server -> Server -> Update Server IP"} + {" "} + to fix the database url connection. + + )} + + +
+
+ { + return ( + + External Port (Internet) + + + + + + ); + }} + /> +
+
+ {!!data?.externalPort && ( +
+
+ + +
+
+ )} -
- -
- - -
-
-
- - ); +
+ +
+ + +
+
+
+ + ); }; 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 cff00a998..545150f87 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 @@ -5,58 +5,58 @@ import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; interface Props { - postgresId: string; + postgresId: string; } export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => { - const { data } = api.postgres.one.useQuery({ postgresId }); - return ( - <> -
- - - Internal Credentials - - -
-
- - -
-
- - -
-
- -
- -
-
-
- - -
+ const { data } = api.postgres.one.useQuery({ postgresId }); + return ( + <> +
+ + + Internal Credentials + + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + +
-
- - -
+
+ + +
-
- - -
-
-
-
-
- - ); +
+ + +
+
+
+
+
+ + ); }; // ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w diff --git a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx index 33ed7a60e..f70cd8c90 100644 --- a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx +++ b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx @@ -28,139 +28,139 @@ import { toast } from "sonner"; import { z } from "zod"; const updatePostgresSchema = z.object({ - name: z.string().min(1, { - message: "Name is required", - }), - description: z.string().optional(), + name: z.string().min(1, { + message: "Name is required", + }), + description: z.string().optional(), }); type UpdatePostgres = z.infer; interface Props { - postgresId: string; + postgresId: string; } export const UpdatePostgres = ({ postgresId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const utils = api.useUtils(); - const { mutateAsync, error, isError, isLoading } = - api.postgres.update.useMutation(); - const { data } = api.postgres.one.useQuery( - { - postgresId, - }, - { - enabled: !!postgresId, - } - ); - const form = useForm({ - defaultValues: { - description: data?.description ?? "", - name: data?.name ?? "", - }, - resolver: zodResolver(updatePostgresSchema), - }); - useEffect(() => { - if (data) { - form.reset({ - description: data.description ?? "", - name: data.name, - }); - } - }, [data, form, form.reset]); + const [isOpen, setIsOpen] = useState(false); + const utils = api.useUtils(); + const { mutateAsync, error, isError, isLoading } = + api.postgres.update.useMutation(); + const { data } = api.postgres.one.useQuery( + { + postgresId, + }, + { + enabled: !!postgresId, + }, + ); + const form = useForm({ + defaultValues: { + description: data?.description ?? "", + name: data?.name ?? "", + }, + resolver: zodResolver(updatePostgresSchema), + }); + useEffect(() => { + if (data) { + form.reset({ + description: data.description ?? "", + name: data.name, + }); + } + }, [data, form, form.reset]); - const onSubmit = async (formData: UpdatePostgres) => { - await mutateAsync({ - name: formData.name, - postgresId: postgresId, - description: formData.description || "", - }) - .then(() => { - toast.success("Postgres updated successfully"); - utils.postgres.one.invalidate({ - postgresId: postgresId, - }); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error updating Postgres"); - }) - .finally(() => {}); - }; + const onSubmit = async (formData: UpdatePostgres) => { + await mutateAsync({ + name: formData.name, + postgresId: postgresId, + description: formData.description || "", + }) + .then(() => { + toast.success("Postgres updated successfully"); + utils.postgres.one.invalidate({ + postgresId: postgresId, + }); + setIsOpen(false); + }) + .catch(() => { + toast.error("Error updating Postgres"); + }) + .finally(() => {}); + }; - return ( - - - - - - - Modify Postgres - Update the Postgres data - - {isError && {error?.message}} + return ( + + + + + + + Modify Postgres + Update the Postgres data + + {isError && {error?.message}} -
-
-
- - ( - - Name - - - +
+
+ + + ( + + Name + + + - - - )} - /> - ( - - Description - -