diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2ac54229..e9591f3c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,7 +12,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - run: pnpm run server:build @@ -26,7 +26,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - run: pnpm run server:build @@ -39,7 +39,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20.9.0 + node-version: 20.16.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - run: pnpm run server:build diff --git a/.nvmrc b/.nvmrc index 43bff1f8..593cb75b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.9.0 \ No newline at end of file +20.16.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a69fa686..0ac5a358 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ feat: add new feature Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. -We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. +We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory. ```bash git clone https://github.com/dokploy/dokploy.git diff --git a/Dockerfile b/Dockerfile index 98ed9851..00043b0c 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 zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/* # Copy only the necessary files COPY --from=build /prod/dokploy/.next ./.next @@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm # Install Nixpacks and tsx # | VERBOSE=1 VERSION=1.21.0 bash -ARG NIXPACKS_VERSION=1.35.0 +ARG NIXPACKS_VERSION=1.39.0 RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ && chmod +x install.sh \ && ./install.sh \ @@ -63,4 +63,4 @@ RUN curl -sSL https://railpack.com/install.sh | bash COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack EXPOSE 3000 -CMD [ "pnpm", "start" ] \ No newline at end of file +CMD [ "pnpm", "start" ] diff --git a/README.md b/README.md index 90c651b0..d192d6f7 100644 --- a/README.md +++ b/README.md @@ -148,19 +148,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). Watch the video - - ## Contributing Check out the [Contributing Guide](CONTRIBUTING.md) for more information. diff --git a/apps/dokploy/.nvmrc b/apps/dokploy/.nvmrc index 43bff1f8..593cb75b 100644 --- a/apps/dokploy/.nvmrc +++ b/apps/dokploy/.nvmrc @@ -1 +1 @@ -20.9.0 \ No newline at end of file +20.16.0 \ No newline at end of file diff --git a/apps/dokploy/Dockerfile b/apps/dokploy/Dockerfile deleted file mode 100644 index f4188c54..00000000 --- a/apps/dokploy/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM node:18-slim AS base -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable - -FROM base AS build -COPY . /usr/src/app -WORKDIR /usr/src/app - - -RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/* - -# Install dependencies -RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile - -# Build only the dokploy app -RUN pnpm run dokploy:build - -# Deploy only the dokploy app -RUN pnpm deploy --filter=dokploy --prod /prod/dokploy - -FROM base AS dokploy -COPY --from=build /prod/dokploy /prod/dokploy -WORKDIR /prod/dokploy -EXPOSE 3000 -CMD [ "pnpm", "start" ] \ No newline at end of file diff --git a/apps/dokploy/__test__/drop/drop.test.test.ts b/apps/dokploy/__test__/drop/drop.test.test.ts index 74a4eb66..b18d7b4b 100644 --- a/apps/dokploy/__test__/drop/drop.test.test.ts +++ b/apps/dokploy/__test__/drop/drop.test.test.ts @@ -105,6 +105,7 @@ const baseApp: ApplicationNested = { ports: [], projectId: "", publishDirectory: null, + isStaticSpa: null, redirects: [], refreshToken: "", registry: null, @@ -149,67 +150,68 @@ describe("unzipDrop using real zip files", () => { } finally { } }); - - it("should correctly extract a zip with a single root folder and a subfolder", async () => { - baseApp.appName = "folderwithfile"; - // const appName = "folderwithfile"; - const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); - const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip"); - - const zipBuffer = zip.toBuffer(); - const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, baseApp); - - const files = await fs.readdir(outputPath, { withFileTypes: true }); - expect(files.some((f) => f.name === "folder1.txt")).toBe(true); - }); - - it("should correctly extract a zip with multiple root folders", async () => { - baseApp.appName = "two-folders"; - // const appName = "two-folders"; - const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); - const zip = new AdmZip("./__test__/drop/zips/two-folders.zip"); - - const zipBuffer = zip.toBuffer(); - const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, baseApp); - - const files = await fs.readdir(outputPath, { withFileTypes: true }); - - expect(files.some((f) => f.name === "folder1")).toBe(true); - expect(files.some((f) => f.name === "folder2")).toBe(true); - }); - - it("should correctly extract a zip with a single root with a file", async () => { - baseApp.appName = "nested"; - // const appName = "nested"; - const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); - const zip = new AdmZip("./__test__/drop/zips/nested.zip"); - - const zipBuffer = zip.toBuffer(); - const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, baseApp); - - const files = await fs.readdir(outputPath, { withFileTypes: true }); - - expect(files.some((f) => f.name === "folder1")).toBe(true); - expect(files.some((f) => f.name === "folder2")).toBe(true); - expect(files.some((f) => f.name === "folder3")).toBe(true); - }); - - it("should correctly extract a zip with a single root with a folder", async () => { - baseApp.appName = "folder-with-sibling-file"; - // const appName = "folder-with-sibling-file"; - const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); - const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip"); - - const zipBuffer = zip.toBuffer(); - const file = new File([zipBuffer], "single.zip"); - await unzipDrop(file, baseApp); - - const files = await fs.readdir(outputPath, { withFileTypes: true }); - - expect(files.some((f) => f.name === "folder1")).toBe(true); - expect(files.some((f) => f.name === "test.txt")).toBe(true); - }); }); + +// it("should correctly extract a zip with a single root folder and a subfolder", async () => { +// baseApp.appName = "folderwithfile"; +// // const appName = "folderwithfile"; +// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); +// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip"); + +// const zipBuffer = zip.toBuffer(); +// const file = new File([zipBuffer], "single.zip"); +// await unzipDrop(file, baseApp); + +// const files = await fs.readdir(outputPath, { withFileTypes: true }); +// expect(files.some((f) => f.name === "folder1.txt")).toBe(true); +// }); + +// it("should correctly extract a zip with multiple root folders", async () => { +// baseApp.appName = "two-folders"; +// // const appName = "two-folders"; +// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); +// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip"); + +// const zipBuffer = zip.toBuffer(); +// const file = new File([zipBuffer], "single.zip"); +// await unzipDrop(file, baseApp); + +// const files = await fs.readdir(outputPath, { withFileTypes: true }); + +// expect(files.some((f) => f.name === "folder1")).toBe(true); +// expect(files.some((f) => f.name === "folder2")).toBe(true); +// }); + +// it("should correctly extract a zip with a single root with a file", async () => { +// baseApp.appName = "nested"; +// // const appName = "nested"; +// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); +// const zip = new AdmZip("./__test__/drop/zips/nested.zip"); + +// const zipBuffer = zip.toBuffer(); +// const file = new File([zipBuffer], "single.zip"); +// await unzipDrop(file, baseApp); + +// const files = await fs.readdir(outputPath, { withFileTypes: true }); + +// expect(files.some((f) => f.name === "folder1")).toBe(true); +// expect(files.some((f) => f.name === "folder2")).toBe(true); +// expect(files.some((f) => f.name === "folder3")).toBe(true); +// }); + +// it("should correctly extract a zip with a single root with a folder", async () => { +// baseApp.appName = "folder-with-sibling-file"; +// // const appName = "folder-with-sibling-file"; +// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); +// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip"); + +// const zipBuffer = zip.toBuffer(); +// const file = new File([zipBuffer], "single.zip"); +// await unzipDrop(file, baseApp); + +// const files = await fs.readdir(outputPath, { withFileTypes: true }); + +// expect(files.some((f) => f.name === "folder1")).toBe(true); +// expect(files.some((f) => f.name === "test.txt")).toBe(true); +// }); +// }); diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 5437e64d..6c136b25 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -85,6 +85,7 @@ const baseApp: ApplicationNested = { ports: [], projectId: "", publishDirectory: null, + isStaticSpa: null, redirects: [], refreshToken: "", registry: null, diff --git a/apps/dokploy/__test__/utils/backups.test.ts b/apps/dokploy/__test__/utils/backups.test.ts index c7bc310c..2c1e5dec 100644 --- a/apps/dokploy/__test__/utils/backups.test.ts +++ b/apps/dokploy/__test__/utils/backups.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, test } from "vitest"; import { normalizeS3Path } from "@dokploy/server/utils/backups/utils"; +import { describe, expect, test } from "vitest"; describe("normalizeS3Path", () => { test("should handle empty and whitespace-only prefix", () => { 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 0e848fec..aa359d67 100644 --- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx @@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => { {templateInfo.template.envs.map((env, index) => (
{env}
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => { Mount File Content - + { case BuildType.static: return { buildType: BuildType.static, + isStaticSpa: data.isStaticSpa ?? false, }; case BuildType.railpack: return { buildType: BuildType.railpack, }; - default: + default: { const buildType = data.buildType as BuildType; return { buildType, } as AddTemplate; + } } }; @@ -174,6 +179,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { data.buildType === BuildType.heroku_buildpacks ? data.herokuVersion : null, + isStaticSpa: + data.buildType === BuildType.static ? data.isStaticSpa : null, }) .then(async () => { toast.success("Build type saved"); @@ -364,6 +371,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { )} /> )} + {buildType === BuildType.static && ( + ( + + +
+ + + Single Page Application (SPA) + +
+
+ +
+ )} + /> + )}
+
-
- -
- -
-					
-				
+
+							
+						
+ + )} ); diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index a8a2f1ae..7617b510 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -39,6 +39,12 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -48,9 +54,9 @@ import { CheckIcon, ChevronsUpDown, Copy, - RotateCcw, - RefreshCw, DatabaseZap, + RefreshCw, + RotateCcw, } from "lucide-react"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -58,12 +64,6 @@ import { toast } from "sonner"; import { z } from "zod"; import type { ServiceType } from "../../application/advanced/show-resources"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; type DatabaseType = | Exclude diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index bb3128cf..28ee68a9 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -1,3 +1,10 @@ +import { + MariadbIcon, + MongodbIcon, + MysqlIcon, + PostgresqlIcon, +} from "@/components/icons/data-tools-icons"; +import { AlertBlock } from "@/components/shared/alert-block"; import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; import { @@ -13,6 +20,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { ClipboardList, @@ -25,17 +33,9 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import type { ServiceType } from "../../application/advanced/show-resources"; -import { RestoreBackup } from "./restore-backup"; -import { HandleBackup } from "./handle-backup"; -import { cn } from "@/lib/utils"; -import { - MariadbIcon, - MongodbIcon, - MysqlIcon, - PostgresqlIcon, -} from "@/components/icons/data-tools-icons"; -import { AlertBlock } from "@/components/shared/alert-block"; import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal"; +import { HandleBackup } from "./handle-backup"; +import { RestoreBackup } from "./restore-backup"; interface Props { id: string; diff --git a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx index 4b89e984..8a9f55c9 100644 --- a/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx +++ b/apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx @@ -1,24 +1,9 @@ "use client"; -import { authClient } from "@/lib/auth-client"; -import { useEffect, useState } from "react"; +import { Logo } from "@/components/shared/logo"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - CheckIcon, - ChevronsUpDown, - Settings2, - UserIcon, - XIcon, - Shield, - Calendar, - Key, - Copy, - Fingerprint, - Building2, - CreditCard, - Server, -} from "lucide-react"; -import { toast } from "sonner"; import { Command, CommandEmpty, @@ -32,19 +17,34 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { cn } from "@/lib/utils"; -import { Logo } from "@/components/shared/logo"; -import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, - TooltipTrigger, TooltipProvider, + TooltipTrigger, } from "@/components/ui/tooltip"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { format } from "date-fns"; -import copy from "copy-to-clipboard"; +import { authClient } from "@/lib/auth-client"; +import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; +import copy from "copy-to-clipboard"; +import { format } from "date-fns"; +import { + Building2, + Calendar, + CheckIcon, + ChevronsUpDown, + Copy, + CreditCard, + Fingerprint, + Key, + Server, + Settings2, + Shield, + UserIcon, + XIcon, +} from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; type User = typeof authClient.$Infer.Session.user; diff --git a/apps/dokploy/components/dashboard/project/duplicate-project.tsx b/apps/dokploy/components/dashboard/project/duplicate-project.tsx index 038ddcb6..ffcfeba8 100644 --- a/apps/dokploy/components/dashboard/project/duplicate-project.tsx +++ b/apps/dokploy/components/dashboard/project/duplicate-project.tsx @@ -10,6 +10,7 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { api } from "@/utils/api"; import { Copy, Loader2 } from "lucide-react"; import { useRouter } from "next/router"; @@ -48,6 +49,7 @@ export const DuplicateProject = ({ const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [description, setDescription] = useState(""); + const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project" const utils = api.useUtils(); const router = useRouter(); @@ -59,9 +61,15 @@ export const DuplicateProject = ({ api.project.duplicate.useMutation({ onSuccess: async (newProject) => { await utils.project.all.invalidate(); - toast.success("Project duplicated successfully"); + toast.success( + duplicateType === "new-project" + ? "Project duplicated successfully" + : "Services duplicated successfully", + ); setOpen(false); - router.push(`/dashboard/project/${newProject.projectId}`); + if (duplicateType === "new-project") { + router.push(`/dashboard/project/${newProject.projectId}`); + } }, onError: (error) => { toast.error(error.message); @@ -69,7 +77,7 @@ export const DuplicateProject = ({ }); const handleDuplicate = async () => { - if (!name) { + if (duplicateType === "new-project" && !name) { toast.error("Project name is required"); return; } @@ -83,6 +91,7 @@ export const DuplicateProject = ({ id: service.id, type: service.type, })), + duplicateInSameProject: duplicateType === "same-project", }); }; @@ -95,6 +104,7 @@ export const DuplicateProject = ({ // Reset form when closing setName(""); setDescription(""); + setDuplicateType("new-project"); } }} > @@ -106,32 +116,54 @@ export const DuplicateProject = ({ - Duplicate Project + Duplicate Services - Create a new project with the selected services + Choose where to duplicate the selected services
- - setName(e.target.value)} - placeholder="New project name" - /> + + +
+ + +
+
+ + +
+
-
- - setDescription(e.target.value)} - placeholder="Project description (optional)" - /> -
+ {duplicateType === "new-project" && ( + <> +
+ + setName(e.target.value)} + placeholder="New project name" + /> +
+ +
+ + setDescription(e.target.value)} + placeholder="Project description (optional)" + /> +
+ + )}
@@ -159,10 +191,14 @@ export const DuplicateProject = ({ {isLoading ? ( <> - Duplicating... + {duplicateType === "new-project" + ? "Duplicating project..." + : "Duplicating services..."} + ) : duplicateType === "new-project" ? ( + "Duplicate project" ) : ( - "Duplicate" + "Duplicate services" )} diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 1a8bc684..59e4736d 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -18,6 +18,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Switch } from "@/components/ui/switch"; import { generateSHA256Hash } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -29,7 +30,6 @@ import { toast } from "sonner"; import { z } from "zod"; import { Disable2FA } from "./disable-2fa"; import { Enable2FA } from "./enable-2fa"; -import { Switch } from "@/components/ui/switch"; const profileSchema = z.object({ email: z.string(), diff --git a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx index 97994145..a2c9b50a 100644 --- a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx @@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => { remotely. +
+

+ You will need to purchase or rent a Virtual Private Server (VPS) to + proceed, we recommend to use one of these providers since has been + heavily tested. +

+ + + You are free to use whatever provider, but we recommend to use one + of the above, to avoid issues. + +
{!canCreateMoreServers && ( You cannot create more servers,{" "} diff --git a/apps/dokploy/components/dashboard/settings/servers/show-schedules-modal.tsx b/apps/dokploy/components/dashboard/settings/servers/show-schedules-modal.tsx index 5f6a26e7..6f6a1a6d 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-schedules-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-schedules-modal.tsx @@ -1,7 +1,7 @@ -import { useState } from "react"; +import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules"; +import { useState } from "react"; interface Props { serverId: string; diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx index 7ad9df94..813acb34 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx @@ -40,10 +40,10 @@ import { HandleServers } from "./handle-servers"; import { SetupServer } from "./setup-server"; import { ShowDockerContainersModal } from "./show-docker-containers-modal"; import { ShowMonitoringModal } from "./show-monitoring-modal"; +import { ShowSchedulesModal } from "./show-schedules-modal"; import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; -import { ShowSchedulesModal } from "./show-schedules-modal"; export const ShowServers = () => { const { t } = useTranslation("settings"); diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx index bab93047..1ec4f2ab 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx @@ -177,6 +177,14 @@ export const WelcomeSuscription = () => { Hostinger - Get 20% Discount +
  • + + American Cloud - Get $20 Credits + +
  • { Cancel Invitation )} - - { - await removeInvitation({ - invitationId: invitation.id, - }).then(() => { - refetch(); - toast.success( - "Invitation removed", - ); - }); - }} - > - Remove Invitation - )} + { + await removeInvitation({ + invitationId: invitation.id, + }).then(() => { + refetch(); + toast.success("Invitation removed"); + }); + }} + > + Remove Invitation + diff --git a/apps/dokploy/components/layouts/dashboard-layout.tsx b/apps/dokploy/components/layouts/dashboard-layout.tsx index 25dd77a5..4e2e15b3 100644 --- a/apps/dokploy/components/layouts/dashboard-layout.tsx +++ b/apps/dokploy/components/layouts/dashboard-layout.tsx @@ -1,6 +1,7 @@ -import Page from "./side"; -import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar"; import { api } from "@/utils/api"; +import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar"; +import Page from "./side"; +import { ChatwootWidget } from "../shared/ChatwootWidget"; interface Props { children: React.ReactNode; @@ -9,10 +10,15 @@ interface Props { export const DashboardLayout = ({ children }: Props) => { const { data: haveRootAccess } = api.user.haveRootAccess.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); return ( <> {children} + {isCloud === true && ( + + )} + {haveRootAccess === true && } ); diff --git a/apps/dokploy/components/layouts/project-layout.tsx b/apps/dokploy/components/layouts/project-layout.tsx deleted file mode 100644 index f5fdf350..00000000 --- a/apps/dokploy/components/layouts/project-layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import Page from "./side"; - -interface Props { - children: React.ReactNode; -} - -export const ProjectLayout = ({ children }: Props) => { - return {children}; -}; diff --git a/apps/dokploy/components/shared/ChatwootWidget.tsx b/apps/dokploy/components/shared/ChatwootWidget.tsx new file mode 100644 index 00000000..6694b13c --- /dev/null +++ b/apps/dokploy/components/shared/ChatwootWidget.tsx @@ -0,0 +1,69 @@ +import Script from "next/script"; +import { useEffect } from "react"; + +interface ChatwootWidgetProps { + websiteToken: string; + baseUrl?: string; + settings?: { + position?: "left" | "right"; + type?: "standard" | "expanded_bubble"; + launcherTitle?: string; + darkMode?: boolean; + hideMessageBubble?: boolean; + placement?: "right" | "left"; + showPopoutButton?: boolean; + widgetStyle?: "standard" | "bubble"; + }; + user?: { + identifier: string; + name?: string; + email?: string; + phoneNumber?: string; + avatarUrl?: string; + customAttributes?: Record; + identifierHash?: string; + }; +} + +export const ChatwootWidget = ({ + websiteToken, + baseUrl = "https://app.chatwoot.com", + settings = { + position: "right", + type: "standard", + launcherTitle: "Chat with us", + }, + user, +}: ChatwootWidgetProps) => { + useEffect(() => { + // Configurar los settings de Chatwoot + window.chatwootSettings = { + position: "right", + }; + + (window as any).chatwootSDKReady = () => { + window.chatwootSDK?.run({ websiteToken, baseUrl }); + + const trySetUser = () => { + if (window.$chatwoot && user) { + window.$chatwoot.setUser(user.identifier, { + email: user.email, + name: user.name, + avatar_url: user.avatarUrl, + phone_number: user.phoneNumber, + }); + } + }; + + trySetUser(); + }; + }, [websiteToken, baseUrl, user, settings]); + + return ( +