diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8584fdf6..c64d0672 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -165,86 +165,8 @@ Thank you for your contribution! ## Templates -To add a new template, go to `templates` folder and create a new folder with the name of the template. +To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file. -Let's take the example of `plausible` template. - -1. create a folder in `templates/plausible` -2. create a `docker-compose.yml` file inside the folder with the content of compose. -3. create a `index.ts` file inside the folder with the following code as base: -4. When creating a pull request, please provide a video of the template working in action. - -```typescript -// EXAMPLE -import { - generateBase64, - generateHash, - generateRandomDomain, - type Template, - type Schema, - type DomainSchema, -} from "../utils"; - -export function generate(schema: Schema): Template { - // do your stuff here, like create a new domain, generate random passwords, mounts. - const mainServiceHash = generateHash(schema.projectName); - const mainDomain = generateRandomDomain(schema); - const secretBase = generateBase64(64); - const toptKeyBase = generateBase64(32); - - const domains: DomainSchema[] = [ - { - host: mainDomain, - port: 8000, - serviceName: "plausible", - }, - ]; - - const envs = [ - `BASE_URL=http://${mainDomain}`, - `SECRET_KEY_BASE=${secretBase}`, - `TOTP_VAULT_KEY=${toptKeyBase}`, - `HASH=${mainServiceHash}`, - ]; - - const mounts: Template["mounts"] = [ - { - filePath: "./clickhouse/clickhouse-config.xml", - content: "some content......", - }, - ]; - - return { - envs, - mounts, - domains, - }; -} -``` - -4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties: - -**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.** - -```typescript -{ - id: "plausible", - name: "Plausible", - version: "v2.1.0", - description: - "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", - logo: "plausible.svg", // we defined the name and the extension of the logo - links: { - github: "https://github.com/plausible/plausible", - website: "https://plausible.io/", - docs: "https://plausible.io/docs", - }, - tags: ["analytics"], - load: () => import("./plausible/index").then((m) => m.generate), -}, -``` - -5. Add the logo or image of the template to `public/templates/plausible.svg` ### Recommendations diff --git a/apps/dokploy/CONTRIBUTING.md b/apps/dokploy/CONTRIBUTING.md deleted file mode 100644 index 8686b98a..00000000 --- a/apps/dokploy/CONTRIBUTING.md +++ /dev/null @@ -1,242 +0,0 @@ - - -# Contributing - -Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute. - - -Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues. - -We have a few guidelines to follow when contributing to this project: - -- [Commit Convention](#commit-convention) -- [Setup](#setup) -- [Development](#development) -- [Build](#build) -- [Pull Request](#pull-request) - -## Commit Convention - -Before you craete a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. - -### Commit Message Format -``` -[optional scope]: - -[optional body] - -[optional footer(s)] -``` - -#### Type -Must be one of the following: - -* **feat**: A new feature -* **fix**: A bug fix -* **docs**: Documentation only changes -* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) -* **refactor**: A code change that neither fixes a bug nor adds a feature -* **perf**: A code change that improves performance -* **test**: Adding missing tests or correcting existing tests -* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) -* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) -* **chore**: Other changes that don't modify `src` or `test` files -* **revert**: Reverts a previous commit - -Example: -``` -feat: add new feature -``` - - - - -## Setup - -Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. - -```bash -git clone https://github.com/dokploy/dokploy.git -cd dokploy -pnpm install -cp .env.example .env -``` - -## Development - -Is required to have **Docker** installed on your machine. - - -### Setup - -Run the command that will spin up all the required services and files. - -```bash -pnpm run setup -``` - -Now run the development server. - -```bash -pnpm run dev -``` - - -Go to http://localhost:3000 to see the development server - -## Build - -```bash -pnpm run build -``` - -## Docker - -To build the docker image -```bash -pnpm run docker:build -``` - -To push the docker image -```bash -pnpm run docker:push -``` - -## Password Reset - -In the case you lost your password, you can reset it using the following command - -```bash -pnpm run reset-password -``` - -If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel` - -```bash -bunx lt --port 3000 -``` - -If you run into permission issues of docker run the following command - -```bash -sudo chown -R USERNAME dokploy or sudo chown -R $(whoami) ~/.docker -``` - -## Application deploy - -In case you want to deploy the application on your machine and you selected nixpacks or buildpacks, you need to install first. - -```bash -# Install Nixpacks -curl -sSL https://nixpacks.com/install.sh -o install.sh \ - && chmod +x install.sh \ - && ./install.sh -``` - -```bash -# Install Buildpacks -curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack -``` - - -## Pull Request - -- The `main` branch is the source of truth and should always reflect the latest stable release. -- Create a new branch for each feature or bug fix. -- Make sure to add tests for your changes. -- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes. -- When creating a pull request, please provide a clear and concise description of the changes made. -- If you include a video or screenshot, would be awesome so we can see the changes in action. -- If your pull request fixes an open issue, please reference the issue in the pull request description. -- Once your pull request is merged, you will be automatically added as a contributor to the project. - -Thank you for your contribution! - - - - - -## Templates - -To add a new template, go to `templates` folder and create a new folder with the name of the template. - -Let's take the example of `plausible` template. - -1. create a folder in `templates/plausible` -2. create a `docker-compose.yml` file inside the folder with the content of compose. -3. create a `index.ts` file inside the folder with the following code as base: -4. When creating a pull request, please provide a video of the template working in action. - -```typescript -// EXAMPLE -import { - generateHash, - generateRandomDomain, - type Template, - type Schema, -} from "../utils"; - - -export function generate(schema: Schema): Template { - - // do your stuff here, like create a new domain, generate random passwords, mounts. - const mainServiceHash = generateHash(schema.projectName); - const randomDomain = generateRandomDomain(schema); - const secretBase = generateBase64(64); - const toptKeyBase = generateBase64(32); - - const envs = [ -// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name. - `PLAUSIBLE_HOST=${randomDomain}`, - "PLAUSIBLE_PORT=8000", - `BASE_URL=http://${randomDomain}`, - `SECRET_KEY_BASE=${secretBase}`, - `TOTP_VAULT_KEY=${toptKeyBase}`, - `HASH=${mainServiceHash}`, - ]; - - const mounts: Template["mounts"] = [ - { - mountPath: "./clickhouse/clickhouse-config.xml", - content: `some content......`, - }, - ]; - - return { - envs, - mounts, - }; -} -``` - -4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties: - -**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.** - -```typescript -{ - id: "plausible", - name: "Plausible", - version: "v2.1.0", - description: - "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", - logo: "plausible.svg", // we defined the name and the extension of the logo - links: { - github: "https://github.com/plausible/plausible", - website: "https://plausible.io/", - docs: "https://plausible.io/docs", - }, - tags: ["analytics"], - load: () => import("./plausible/index").then((m) => m.generate), -}, -``` - -5. Add the logo or image of the template to `public/templates/plausible.svg` - - -### Recomendations -- Use the same name of the folder as the id of the template. -- The logo should be in the public folder. -- If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name. -- Test first on a vps or a server to make sure the template works. - diff --git a/apps/dokploy/__test__/compose/domain/labels.test.ts b/apps/dokploy/__test__/compose/domain/labels.test.ts index 8bc9fbcc..c5f45810 100644 --- a/apps/dokploy/__test__/compose/domain/labels.test.ts +++ b/apps/dokploy/__test__/compose/domain/labels.test.ts @@ -9,6 +9,7 @@ describe("createDomainLabels", () => { port: 8080, https: false, uniqueConfigKey: 1, + customCertResolver: null, certificateType: "none", applicationId: "", composeId: "", diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts index 4e6f20d3..f5b68c1d 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.test.ts @@ -27,6 +27,7 @@ if (typeof window === "undefined") { const baseApp: ApplicationNested = { applicationId: "", herokuVersion: "", + watchPaths: [], applicationStatus: "done", appName: "", autoDeploy: true, @@ -37,6 +38,7 @@ const baseApp: ApplicationNested = { isPreviewDeploymentsActive: false, previewBuildArgs: null, previewCertificateType: "none", + previewCustomCertResolver: null, previewEnv: null, previewHttps: false, previewPath: "/", diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts new file mode 100644 index 00000000..902e3163 --- /dev/null +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -0,0 +1,425 @@ +import { describe, expect, it } from "vitest"; +import type { CompleteTemplate } from "@dokploy/server/templates/processors"; +import { processTemplate } from "@dokploy/server/templates/processors"; +import type { Schema } from "@dokploy/server/templates"; + +describe("processTemplate", () => { + // Mock schema for testing + const mockSchema: Schema = { + projectName: "test", + serverIp: "127.0.0.1", + }; + + describe("variables processing", () => { + it("should process basic variables with utility functions", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + secret_base: "${base64:64}", + totp_key: "${base64:32}", + password: "${password:32}", + hash: "${hash:16}", + }, + config: { + domains: [], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(0); + expect(result.domains).toHaveLength(0); + expect(result.mounts).toHaveLength(0); + }); + + it("should allow referencing variables in other variables", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + api_domain: "api.${main_domain}", + }, + config: { + domains: [], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(0); + expect(result.domains).toHaveLength(0); + expect(result.mounts).toHaveLength(0); + }); + }); + + describe("domains processing", () => { + it("should process domains with explicit host", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + }, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + host: "${main_domain}", + }, + ], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.domains).toHaveLength(1); + const domain = result.domains[0]; + expect(domain).toBeDefined(); + if (!domain) return; + expect(domain).toMatchObject({ + serviceName: "plausible", + port: 8000, + }); + expect(domain.host).toBeDefined(); + expect(domain.host).toContain(mockSchema.projectName); + }); + + it("should generate random domain if host is not specified", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + }, + ], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.domains).toHaveLength(1); + const domain = result.domains[0]; + expect(domain).toBeDefined(); + if (!domain || !domain.host) return; + expect(domain.host).toBeDefined(); + expect(domain.host).toContain(mockSchema.projectName); + }); + + it("should allow using ${domain} directly in host", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + host: "${domain}", + }, + ], + env: {}, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.domains).toHaveLength(1); + const domain = result.domains[0]; + expect(domain).toBeDefined(); + if (!domain || !domain.host) return; + expect(domain.host).toBeDefined(); + expect(domain.host).toContain(mockSchema.projectName); + }); + }); + + describe("environment variables processing", () => { + it("should process env vars with variable references", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + secret_base: "${base64:64}", + }, + config: { + domains: [], + env: { + BASE_URL: "http://${main_domain}", + SECRET_KEY_BASE: "${secret_base}", + }, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(2); + const baseUrl = result.envs.find((env: string) => + env.startsWith("BASE_URL="), + ); + const secretKey = result.envs.find((env: string) => + env.startsWith("SECRET_KEY_BASE="), + ); + + expect(baseUrl).toBeDefined(); + expect(secretKey).toBeDefined(); + if (!baseUrl || !secretKey) return; + + expect(baseUrl).toContain(mockSchema.projectName); + const base64Value = secretKey.split("=")[1]; + expect(base64Value).toBeDefined(); + if (!base64Value) return; + expect(base64Value).toMatch(/^[A-Za-z0-9+/]+={0,2}$/); + expect(base64Value.length).toBeGreaterThanOrEqual(86); + expect(base64Value.length).toBeLessThanOrEqual(88); + }); + + it("should process env vars when provided as an array", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: [ + 'CLOUDFLARE_TUNNEL_TOKEN=""', + 'ANOTHER_VAR="some value"', + "DOMAIN=${domain}", + ], + mounts: [], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(3); + + // Should preserve exact format for static values + expect(result.envs[0]).toBe('CLOUDFLARE_TUNNEL_TOKEN=""'); + expect(result.envs[1]).toBe('ANOTHER_VAR="some value"'); + + // Should process variables in array items + expect(result.envs[2]).toContain(mockSchema.projectName); + }); + + it("should allow using utility functions directly in env vars", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: { + RANDOM_DOMAIN: "${domain}", + SECRET_KEY: "${base64:32}", + }, + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(2); + const randomDomainEnv = result.envs.find((env: string) => + env.startsWith("RANDOM_DOMAIN="), + ); + const secretKeyEnv = result.envs.find((env: string) => + env.startsWith("SECRET_KEY="), + ); + expect(randomDomainEnv).toBeDefined(); + expect(secretKeyEnv).toBeDefined(); + if (!randomDomainEnv || !secretKeyEnv) return; + + expect(randomDomainEnv).toContain(mockSchema.projectName); + const base64Value = secretKeyEnv.split("=")[1]; + expect(base64Value).toBeDefined(); + if (!base64Value) return; + expect(base64Value).toMatch(/^[A-Za-z0-9+/]+={0,2}$/); + expect(base64Value.length).toBeGreaterThanOrEqual(42); + expect(base64Value.length).toBeLessThanOrEqual(44); + }); + }); + + describe("mounts processing", () => { + it("should process mounts with variable references", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + config_path: "/etc/config", + secret_key: "${base64:32}", + }, + config: { + domains: [], + env: {}, + mounts: [ + { + filePath: "${config_path}/config.xml", + content: "secret_key=${secret_key}", + }, + ], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.mounts).toHaveLength(1); + const mount = result.mounts[0]; + expect(mount).toBeDefined(); + if (!mount) return; + expect(mount.filePath).toContain("/etc/config"); + expect(mount.content).toMatch(/secret_key=[A-Za-z0-9+/]{32}/); + }); + + it("should allow using utility functions directly in mount content", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [], + env: {}, + mounts: [ + { + filePath: "/config/secrets.txt", + content: "random_domain=${domain}\nsecret=${base64:32}", + }, + ], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.mounts).toHaveLength(1); + const mount = result.mounts[0]; + expect(mount).toBeDefined(); + if (!mount) return; + expect(mount.content).toContain(mockSchema.projectName); + expect(mount.content).toMatch(/secret=[A-Za-z0-9+/]{32}/); + }); + }); + + describe("complex template processing", () => { + it("should process a complete template with all features", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + main_domain: "${domain}", + secret_base: "${base64:64}", + totp_key: "${base64:32}", + }, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + host: "${main_domain}", + }, + { + serviceName: "api", + port: 3000, + host: "api.${main_domain}", + }, + ], + env: { + BASE_URL: "http://${main_domain}", + SECRET_KEY_BASE: "${secret_base}", + TOTP_VAULT_KEY: "${totp_key}", + }, + mounts: [ + { + filePath: "/config/app.conf", + content: ` + domain=\${main_domain} + secret=\${secret_base} + totp=\${totp_key} + `, + }, + ], + }, + }; + + const result = processTemplate(template, mockSchema); + + // Check domains + expect(result.domains).toHaveLength(2); + const [domain1, domain2] = result.domains; + expect(domain1).toBeDefined(); + expect(domain2).toBeDefined(); + if (!domain1 || !domain2) return; + expect(domain1.host).toBeDefined(); + expect(domain1.host).toContain(mockSchema.projectName); + expect(domain2.host).toContain("api."); + expect(domain2.host).toContain(mockSchema.projectName); + + // Check env vars + expect(result.envs).toHaveLength(3); + const baseUrl = result.envs.find((env: string) => + env.startsWith("BASE_URL="), + ); + const secretKey = result.envs.find((env: string) => + env.startsWith("SECRET_KEY_BASE="), + ); + const totpKey = result.envs.find((env: string) => + env.startsWith("TOTP_VAULT_KEY="), + ); + + expect(baseUrl).toBeDefined(); + expect(secretKey).toBeDefined(); + expect(totpKey).toBeDefined(); + if (!baseUrl || !secretKey || !totpKey) return; + + expect(baseUrl).toContain(mockSchema.projectName); + + // Check base64 lengths and format + const secretKeyValue = secretKey.split("=")[1]; + const totpKeyValue = totpKey.split("=")[1]; + + expect(secretKeyValue).toBeDefined(); + expect(totpKeyValue).toBeDefined(); + if (!secretKeyValue || !totpKeyValue) return; + + expect(secretKeyValue).toMatch(/^[A-Za-z0-9+/]+={0,2}$/); + expect(secretKeyValue.length).toBeGreaterThanOrEqual(86); + expect(secretKeyValue.length).toBeLessThanOrEqual(88); + + expect(totpKeyValue).toMatch(/^[A-Za-z0-9+/]+={0,2}$/); + expect(totpKeyValue.length).toBeGreaterThanOrEqual(42); + expect(totpKeyValue.length).toBeLessThanOrEqual(44); + + // Check mounts + expect(result.mounts).toHaveLength(1); + const mount = result.mounts[0]; + expect(mount).toBeDefined(); + if (!mount) return; + expect(mount.content).toContain(mockSchema.projectName); + expect(mount.content).toMatch(/secret=[A-Za-z0-9+/]{86,88}/); + expect(mount.content).toMatch(/totp=[A-Za-z0-9+/]{42,44}/); + }); + }); + + describe("Should populate envs, domains and mounts in the case we didn't used any variable", () => { + it("should populate envs, domains and mounts in the case we didn't used any variable", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: {}, + config: { + domains: [ + { + serviceName: "plausible", + port: 8000, + host: "${hash}", + }, + ], + env: { + BASE_URL: "http://${domain}", + SECRET_KEY_BASE: "${password:32}", + TOTP_VAULT_KEY: "${base64:128}", + }, + mounts: [ + { + filePath: "/config/secrets.txt", + content: "random_domain=${domain}\nsecret=${password:32}", + }, + ], + }, + }; + + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(3); + expect(result.domains).toHaveLength(1); + expect(result.mounts).toHaveLength(1); + }); + }); +}); diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index c72d7254..f33b37fd 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -47,7 +47,7 @@ const baseAdmin: User = { letsEncryptEmail: null, sshPrivateKey: null, enableDockerCleanup: false, - enableLogRotation: false, + logCleanupCron: null, serversQuantity: 0, stripeCustomerId: "", stripeSubscriptionId: "", diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 955103de..a64103ff 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -14,6 +14,7 @@ const baseApp: ApplicationNested = { branch: null, dockerBuildStage: "", registryUrl: "", + watchPaths: [], buildArgs: null, isPreviewDeploymentsActive: false, previewBuildArgs: null, @@ -23,6 +24,7 @@ const baseApp: ApplicationNested = { previewPath: "/", previewPort: 3000, previewLimit: 0, + previewCustomCertResolver: null, previewWildcard: "", project: { env: "", @@ -103,6 +105,7 @@ const baseDomain: Domain = { port: null, serviceName: "", composeId: "", + customCertResolver: null, domainType: "application", uniqueConfigKey: 1, previewDeploymentId: "", diff --git a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx new file mode 100644 index 00000000..2a3f2f43 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx @@ -0,0 +1,347 @@ +import { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Code2, Globe2, HardDrive } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { AlertBlock } from "@/components/shared/alert-block"; + +const ImportSchema = z.object({ + base64: z.string(), +}); + +type ImportType = z.infer; + +interface Props { + composeId: string; +} + +export const ShowImport = ({ composeId }: Props) => { + const [showModal, setShowModal] = useState(false); + const [showMountContent, setShowMountContent] = useState(false); + const [selectedMount, setSelectedMount] = useState<{ + filePath: string; + content: string; + } | null>(null); + const [templateInfo, setTemplateInfo] = useState<{ + compose: string; + template: { + domains: Array<{ + serviceName: string; + port: number; + path?: string; + host?: string; + }>; + envs: string[]; + mounts: Array<{ + filePath: string; + content: string; + }>; + }; + } | null>(null); + + const utils = api.useUtils(); + const { mutateAsync: processTemplate, isLoading: isLoadingTemplate } = + api.compose.processTemplate.useMutation(); + const { + mutateAsync: importTemplate, + isLoading: isImporting, + isSuccess: isImportSuccess, + } = api.compose.import.useMutation(); + + const form = useForm({ + defaultValues: { + base64: "", + }, + resolver: zodResolver(ImportSchema), + }); + + useEffect(() => { + form.reset({ + base64: "", + }); + }, [isImportSuccess]); + + const onSubmit = async () => { + const base64 = form.getValues("base64"); + if (!base64) { + toast.error("Please enter a base64 template"); + return; + } + + try { + await importTemplate({ + composeId, + base64, + }); + toast.success("Template imported successfully"); + await utils.compose.one.invalidate({ + composeId, + }); + setShowModal(false); + } catch (_error) { + toast.error("Error importing template"); + } + }; + + const handleLoadTemplate = async () => { + const base64 = form.getValues("base64"); + if (!base64) { + toast.error("Please enter a base64 template"); + return; + } + + try { + const result = await processTemplate({ + composeId, + base64, + }); + setTemplateInfo(result); + setShowModal(true); + } catch (_error) { + toast.error("Error processing template"); + } + }; + + const handleShowMountContent = (mount: { + filePath: string; + content: string; + }) => { + setSelectedMount(mount); + setShowMountContent(true); + }; + + return ( + <> + + + Import + Import your Template configuration + + + + Warning: Importing a template will remove all existing environment + variables, mounts, and domains from this service. + +
+ + ( + + Configuration (Base64) + +