diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index dd91309d3..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,119 +0,0 @@ -version: 2.1 - -jobs: - build-amd64: - machine: - image: ubuntu-2004:current - steps: - - checkout - - run: - name: Prepare .env file - command: | - cp apps/dokploy/.env.production.example .env.production - cp apps/dokploy/.env.production.example apps/dokploy/.env.production - - - run: - name: Build and push AMD64 image - command: | - docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN - if [ "${CIRCLE_BRANCH}" == "main" ]; then - TAG="latest" - elif [ "${CIRCLE_BRANCH}" == "canary" ]; then - TAG="canary" - else - TAG="feature" - fi - docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 . - docker push dokploy/dokploy:${TAG}-amd64 - - build-arm64: - machine: - image: ubuntu-2004:current - resource_class: arm.large - steps: - - checkout - - run: - name: Prepare .env file - command: | - cp apps/dokploy/.env.production.example .env.production - cp apps/dokploy/.env.production.example apps/dokploy/.env.production - - run: - name: Build and push ARM64 image - command: | - docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN - if [ "${CIRCLE_BRANCH}" == "main" ]; then - TAG="latest" - elif [ "${CIRCLE_BRANCH}" == "canary" ]; then - TAG="canary" - else - TAG="feature" - fi - docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 . - docker push dokploy/dokploy:${TAG}-arm64 - - combine-manifests: - docker: - - image: cimg/node:18.18.0 - steps: - - checkout - - setup_remote_docker - - run: - name: Create and push multi-arch manifest - command: | - docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN - - if [ "${CIRCLE_BRANCH}" == "main" ]; then - VERSION=$(node -p "require('./apps/dokploy/package.json').version") - echo $VERSION - TAG="latest" - - docker manifest create dokploy/dokploy:${TAG} \ - dokploy/dokploy:${TAG}-amd64 \ - dokploy/dokploy:${TAG}-arm64 - docker manifest push dokploy/dokploy:${TAG} - - docker manifest create dokploy/dokploy:${VERSION} \ - dokploy/dokploy:${TAG}-amd64 \ - dokploy/dokploy:${TAG}-arm64 - docker manifest push dokploy/dokploy:${VERSION} - elif [ "${CIRCLE_BRANCH}" == "canary" ]; then - TAG="canary" - docker manifest create dokploy/dokploy:${TAG} \ - dokploy/dokploy:${TAG}-amd64 \ - dokploy/dokploy:${TAG}-arm64 - docker manifest push dokploy/dokploy:${TAG} - else - TAG="feature" - docker manifest create dokploy/dokploy:${TAG} \ - dokploy/dokploy:${TAG}-amd64 \ - dokploy/dokploy:${TAG}-arm64 - docker manifest push dokploy/dokploy:${TAG} - fi - -workflows: - build-all: - jobs: - - build-amd64: - filters: - branches: - only: - - main - - canary - - 379-preview-deployment - - build-arm64: - filters: - branches: - only: - - main - - canary - - 379-preview-deployment - - combine-manifests: - requires: - - build-amd64 - - build-arm64 - filters: - branches: - only: - - main - - canary - - 379-preview-deployment diff --git a/.github/sponsors/its.png b/.github/sponsors/its.png new file mode 100644 index 000000000..85e7ad71a Binary files /dev/null and b/.github/sponsors/its.png differ diff --git a/.github/sponsors/light-node.webp b/.github/sponsors/light-node.webp new file mode 100644 index 000000000..56729452c Binary files /dev/null and b/.github/sponsors/light-node.webp differ diff --git a/.github/workflows/create-pr.yml b/.github/workflows/create-pr.yml new file mode 100644 index 000000000..e3f6aa234 --- /dev/null +++ b/.github/workflows/create-pr.yml @@ -0,0 +1,83 @@ +name: Auto PR to main when version changes + +on: + push: + branches: + - canary + +permissions: + contents: write + pull-requests: write + +jobs: + create-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version from package.json + id: package_version + run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV + + - name: Get latest GitHub tag + id: latest_tag + run: | + LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1) + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + echo $LATEST_TAG + - name: Compare versions + id: compare_versions + run: | + if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then + VERSION_CHANGED="true" + else + VERSION_CHANGED="false" + fi + echo "VERSION_CHANGED=$VERSION_CHANGED" >> $GITHUB_ENV + echo "Comparing versions:" + echo "Current version: ${{ env.VERSION }}" + echo "Latest tag: ${{ env.LATEST_TAG }}" + echo "Version changed: $VERSION_CHANGED" + - name: Check if a PR already exists + id: check_pr + run: | + PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length') + echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV + env: + GH_TOKEN: ${{ secrets.GH_PAT }} + + - name: Create Pull Request + if: env.VERSION_CHANGED == 'true' && env.PR_EXISTS == '0' + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git fetch origin main + git checkout canary + git push origin canary + + gh pr create \ + --title "🚀 Release ${{ env.VERSION }}" \ + --body ' + This PR promotes changes from `canary` to `main` for version ${{ env.VERSION }}. + + ### 🔍 Changes Include: + - Version bump to ${{ env.VERSION }} + - All changes from canary branch + + ### ✅ Pre-merge Checklist: + - [ ] All tests passing + - [ ] Documentation updated + - [ ] Docker images built and tested + + > 🤖 This PR was automatically generated by [GitHub Actions](https://github.com/actions)' \ + --base main \ + --head canary \ + --label "release" --label "automated pr" || true \ + --reviewer siumauricio \ + --assignee siumauricio + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml new file mode 100644 index 000000000..2ba2e84ca --- /dev/null +++ b/.github/workflows/dokploy.yml @@ -0,0 +1,161 @@ +name: Dokploy Docker Build + +on: + push: + branches: [main, canary, feat/github-runners] + +env: + IMAGE_NAME: dokploy/dokploy + +jobs: + docker-amd: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set tag and version + id: meta + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + else + TAG="feature" + fi + echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT + + - name: Prepare env file + run: | + cp apps/dokploy/.env.production.example .env.production + cp apps/dokploy/.env.production.example apps/dokploy/.env.production + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + docker-arm: + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set tag and version + id: meta + run: | + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + else + TAG="feature" + fi + echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT + + - name: Prepare env file + run: | + cp apps/dokploy/.env.production.example .env.production + cp apps/dokploy/.env.production.example apps/dokploy/.env.production + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + + combine-manifests: + needs: [docker-amd, docker-arm] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create and push manifests + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + TAG="latest" + + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + docker buildx imagetools create -t ${IMAGE_NAME}:${VERSION} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + else + TAG="feature" + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + fi + + generate-release: + needs: [combine-manifests] + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version + id: get_version + run: | + VERSION=$(node -p "require('./apps/dokploy/package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_version.outputs.version }} + name: ${{ steps.get_version.outputs.version }} + generate_release_notes: true + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ad409e69f..2ac542296 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: 18.18.0 + node-version: 20.9.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: 18.18.0 + node-version: 20.9.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: 18.18.0 + node-version: 20.9.0 cache: "pnpm" - run: pnpm install --frozen-lockfile - run: pnpm run server:build diff --git a/.gitignore b/.gitignore index 368c5ed01..b96c9e37a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ yarn-debug.log* yarn-error.log* # Editor -.vscode .idea # Misc diff --git a/.nvmrc b/.nvmrc index 67a228a44..43bff1f8c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.18.0 \ No newline at end of file +20.9.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91bb70494..19ee38dc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,12 +14,10 @@ We have a few guidelines to follow when contributing to this project: ## Commit Convention - Before you create 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]: @@ -54,6 +52,8 @@ 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 + ```bash git clone https://github.com/dokploy/dokploy.git cd dokploy @@ -73,9 +73,10 @@ Run the command that will spin up all the required services and files. pnpm run dokploy:setup ``` -Run this script +Run this script + ```bash -pnpm run server:script +pnpm run server:script ``` Now run the development server. @@ -169,6 +170,7 @@ Let's take the example of `plausible` template. ```typescript // EXAMPLE import { + generateBase64, generateHash, generateRandomDomain, type Template, @@ -200,8 +202,8 @@ export function generate(schema: Schema): Template { const mounts: Template["mounts"] = [ { - mountPath: "./clickhouse/clickhouse-config.xml", - content: `some content......`, + filePath: "./clickhouse/clickhouse-config.xml", + content: "some content......", }, ]; @@ -247,4 +249,3 @@ export function generate(schema: Schema): Template { ## Docs & Website To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website). - diff --git a/Dockerfile b/Dockerfile index 74b70db0e..986ceb595 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-slim AS base +FROM node:20-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable @@ -7,7 +7,7 @@ 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/* +RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/* # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile @@ -29,7 +29,7 @@ WORKDIR /app # Set production ENV NODE_ENV=production -RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/* # Copy only the necessary files COPY --from=build /prod/dokploy/.next ./.next @@ -48,6 +48,8 @@ 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.29.1 RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ && chmod +x install.sh \ && ./install.sh \ diff --git a/Dockerfile.cloud b/Dockerfile.cloud index 020ea3d69..2cc050021 100644 --- a/Dockerfile.cloud +++ b/Dockerfile.cloud @@ -1,4 +1,4 @@ -FROM node:18-slim AS base +FROM node:20-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable @@ -7,7 +7,7 @@ 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/* +RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/* # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile diff --git a/Dockerfile.schedule b/Dockerfile.schedule index 5eca3420e..d04af70f2 100644 --- a/Dockerfile.schedule +++ b/Dockerfile.schedule @@ -1,4 +1,4 @@ -FROM node:18-slim AS base +FROM node:20-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable @@ -7,7 +7,7 @@ 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/* +RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/* # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile diff --git a/Dockerfile.server b/Dockerfile.server index a25b22e52..98f74966f 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -1,4 +1,4 @@ -FROM node:18-slim AS base +FROM node:20-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable @@ -7,7 +7,7 @@ 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/* +RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/* # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile diff --git a/README.md b/README.md index ff25f7d0d..ca48852c3 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,9 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). Mandarin + + Lightnode + ### Premium Supporters 🥇 @@ -89,6 +92,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). Lightspeed.run Cloudblast.io Startupfame +Itsdb-center ### Community Backers 🤝 diff --git a/apps/api/package.json b/apps/api/package.json index 0da226afa..56ea56952 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "PORT=4000 tsx watch src/index.ts", "build": "tsc --project tsconfig.json", - "start": "node --experimental-specifier-resolution=node dist/index.js", + "start": "node dist/index.js", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index bf91b0400..4b405e9c7 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -4,9 +4,9 @@ import "dotenv/config"; import { zValidator } from "@hono/zod-validator"; import { Queue } from "@nerimity/mimiqueue"; import { createClient } from "redis"; -import { logger } from "./logger"; -import { type DeployJob, deployJobSchema } from "./schema"; -import { deploy } from "./utils"; +import { logger } from "./logger.js"; +import { type DeployJob, deployJobSchema } from "./schema.js"; +import { deploy } from "./utils.js"; const app = new Hono(); const redisClient = createClient({ diff --git a/apps/dokploy/.nvmrc b/apps/dokploy/.nvmrc index 67a228a44..43bff1f8c 100644 --- a/apps/dokploy/.nvmrc +++ b/apps/dokploy/.nvmrc @@ -1 +1 @@ -18.18.0 \ No newline at end of file +20.9.0 \ No newline at end of file diff --git a/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts new file mode 100644 index 000000000..18d7619ab --- /dev/null +++ b/apps/dokploy/__test__/deploy/github.test.ts @@ -0,0 +1,98 @@ +import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; +import { describe, expect, it } from "vitest"; + +describe("GitHub Webhook Skip CI", () => { + const mockGithubHeaders = { + "x-github-event": "push", + }; + + const createMockBody = (message: string) => ({ + head_commit: { + message, + }, + }); + + const skipKeywords = [ + "[skip ci]", + "[ci skip]", + "[no ci]", + "[skip actions]", + "[actions skip]", + ]; + + it("should detect skip keywords in commit message", () => { + for (const keyword of skipKeywords) { + const message = `feat: add new feature ${keyword}`; + const commitMessage = extractCommitMessage( + mockGithubHeaders, + createMockBody(message), + ); + expect(commitMessage.includes(keyword)).toBe(true); + } + }); + + it("should not detect skip keywords in normal commit message", () => { + const message = "feat: add new feature"; + const commitMessage = extractCommitMessage( + mockGithubHeaders, + createMockBody(message), + ); + for (const keyword of skipKeywords) { + expect(commitMessage.includes(keyword)).toBe(false); + } + }); + + it("should handle different webhook sources", () => { + // GitHub + expect( + extractCommitMessage( + { "x-github-event": "push" }, + { head_commit: { message: "[skip ci] test" } }, + ), + ).toBe("[skip ci] test"); + + // GitLab + expect( + extractCommitMessage( + { "x-gitlab-event": "push" }, + { commits: [{ message: "[skip ci] test" }] }, + ), + ).toBe("[skip ci] test"); + + // Bitbucket + expect( + extractCommitMessage( + { "x-event-key": "repo:push" }, + { + push: { + changes: [{ new: { target: { message: "[skip ci] test" } } }], + }, + }, + ), + ).toBe("[skip ci] test"); + + // Gitea + expect( + extractCommitMessage( + { "x-gitea-event": "push" }, + { commits: [{ message: "[skip ci] test" }] }, + ), + ).toBe("[skip ci] test"); + }); + + it("should handle missing commit message", () => { + expect(extractCommitMessage(mockGithubHeaders, {})).toBe("NEW COMMIT"); + expect(extractCommitMessage({ "x-gitlab-event": "push" }, {})).toBe( + "NEW COMMIT", + ); + expect( + extractCommitMessage( + { "x-event-key": "repo:push" }, + { push: { changes: [] } }, + ), + ).toBe("NEW COMMIT"); + expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe( + "NEW COMMIT", + ); + }); +}); 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 c966748a7..fac90cc72 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -14,6 +14,9 @@ import { import { beforeEach, expect, test, vi } from "vitest"; const baseAdmin: Admin = { + cleanupCacheApplications: false, + cleanupCacheOnCompose: false, + cleanupCacheOnPreviews: false, createdAt: "", authId: "", adminId: "string", diff --git a/apps/dokploy/__test__/vitest.config.ts b/apps/dokploy/__test__/vitest.config.ts index 14eabf695..ddc84d6ac 100644 --- a/apps/dokploy/__test__/vitest.config.ts +++ b/apps/dokploy/__test__/vitest.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ NODE: "test", }, }, + plugins: [tsconfigPaths()], resolve: { alias: { "@dokploy/server": path.resolve( diff --git a/apps/dokploy/components/auth/login-2fa.tsx b/apps/dokploy/components/auth/login-2fa.tsx index 7c4915fa6..6a11268e0 100644 --- a/apps/dokploy/components/auth/login-2fa.tsx +++ b/apps/dokploy/components/auth/login-2fa.tsx @@ -13,10 +13,12 @@ import { CardTitle } from "@/components/ui/card"; import { InputOTP, InputOTPGroup, + InputOTPSeparator, InputOTPSlot, } from "@/components/ui/input-otp"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; +import { REGEXP_ONLY_DIGITS } from "input-otp"; import { AlertTriangle } from "lucide-react"; import { useRouter } from "next/router"; import { useEffect } from "react"; @@ -87,25 +89,31 @@ export const Login2FA = ({ authId }: Props) => { )} - 2FA Setup + 2FA Login ( - + Pin - - - - - - - - - - +
+ + + + + + + + + + +
Please enter the 6 digits code provided by your authenticator diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index 6750527d2..9b71a042a 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -259,7 +259,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => { refetch(); }) .catch(() => { - toast.error("Error to update the swarm settings"); + toast.error("Error updating the swarm settings"); }); }; return ( diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx index d8d32e19f..cf7314cf6 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -94,7 +94,7 @@ export const ShowClusterSettings = ({ applicationId }: Props) => { }); }) .catch(() => { - toast.error("Error to update the command"); + toast.error("Error updating the command"); }); }; diff --git a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx index 979660bcc..4cd839a11 100644 --- a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx @@ -71,7 +71,7 @@ export const AddCommand = ({ applicationId }: Props) => { }); }) .catch(() => { - toast.error("Error to update the command"); + toast.error("Error updating the command"); }); }; @@ -81,7 +81,8 @@ export const AddCommand = ({ applicationId }: Props) => {
Run Command - Run a custom command in the container + Run a custom command in the container after the application + initialized
diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/delete-port.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/delete-port.tsx deleted file mode 100644 index cc2c77760..000000000 --- a/apps/dokploy/components/dashboard/application/advanced/ports/delete-port.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { TrashIcon } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - portId: string; -} - -export const DeletePort = ({ portId }: Props) => { - const utils = api.useUtils(); - const { mutateAsync, isLoading } = api.port.delete.useMutation(); - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the port - - - - Cancel - { - await mutateAsync({ - portId, - }) - .then((data) => { - utils.application.one.invalidate({ - applicationId: data?.applicationId, - }); - - toast.success("Port delete succesfully"); - }) - .catch(() => { - toast.error("Error to delete the port"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/add-port.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx similarity index 82% rename from apps/dokploy/components/dashboard/application/advanced/ports/add-port.tsx rename to apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx index 1b613704a..c9758e37f 100644 --- a/apps/dokploy/components/dashboard/application/advanced/ports/add-port.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/ports/handle-ports.tsx @@ -27,7 +27,7 @@ import { } from "@/components/ui/select"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PlusIcon } from "lucide-react"; +import { PenBoxIcon, PlusIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -45,18 +45,29 @@ type AddPort = z.infer; interface Props { applicationId: string; + portId?: string; children?: React.ReactNode; } -export const AddPort = ({ +export const HandlePorts = ({ applicationId, + portId, children = , }: Props) => { const [isOpen, setIsOpen] = useState(false); const utils = api.useUtils(); - const { mutateAsync, isLoading, error, isError } = - api.port.create.useMutation(); + const { data } = api.port.one.useQuery( + { + portId: portId ?? "", + }, + { + enabled: !!portId, + }, + ); + const { mutateAsync, isLoading, error, isError } = portId + ? api.port.update.useMutation() + : api.port.create.useMutation(); const form = useForm({ defaultValues: { @@ -68,32 +79,46 @@ export const AddPort = ({ useEffect(() => { form.reset({ - publishedPort: 0, - targetPort: 0, + publishedPort: data?.publishedPort ?? 0, + targetPort: data?.targetPort ?? 0, + protocol: data?.protocol ?? "tcp", }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); + }, [form, form.reset, form.formState.isSubmitSuccessful, data]); const onSubmit = async (data: AddPort) => { await mutateAsync({ applicationId, ...data, + portId: portId || "", }) .then(async () => { - toast.success("Port Created"); + toast.success(portId ? "Port Updated" : "Port Created"); await utils.application.one.invalidate({ applicationId, }); setIsOpen(false); }) .catch(() => { - toast.error("Error to create the port"); + toast.error( + portId ? "Error updating the port" : "Error creating the port", + ); }); }; return ( - + {portId ? ( + + ) : ( + + )} @@ -204,7 +229,7 @@ export const AddPort = ({ form="hook-form-add-port" type="submit" > - Create + {portId ? "Update" : "Create"} diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx index 1ab804fb4..a2c6ddcf1 100644 --- a/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/ports/show-port.tsx @@ -1,4 +1,6 @@ import { AlertBlock } from "@/components/shared/alert-block"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -7,23 +9,25 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Rss } from "lucide-react"; +import { Rss, Trash2 } from "lucide-react"; import React from "react"; -import { AddPort } from "./add-port"; -import { DeletePort } from "./delete-port"; -import { UpdatePort } from "./update-port"; +import { toast } from "sonner"; +import { HandlePorts } from "./handle-ports"; interface Props { applicationId: string; } export const ShowPorts = ({ applicationId }: Props) => { - const { data } = api.application.one.useQuery( + const { data, refetch } = api.application.one.useQuery( { applicationId, }, { enabled: !!applicationId }, ); + const { mutateAsync: deletePort, isLoading: isRemoving } = + api.port.delete.useMutation(); + return ( @@ -35,7 +39,7 @@ export const ShowPorts = ({ applicationId }: Props) => { {data && data?.ports.length > 0 && ( - Add Port + Add Port )} @@ -45,7 +49,7 @@ export const ShowPorts = ({ applicationId }: Props) => { No ports configured - Add Port + Add Port ) : (
@@ -78,8 +82,36 @@ export const ShowPorts = ({ applicationId }: Props) => {
- - + + { + await deletePort({ + portId: port.portId, + }) + .then(() => { + refetch(); + toast.success("Port deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting port"); + }); + }} + > + +
diff --git a/apps/dokploy/components/dashboard/application/advanced/ports/update-port.tsx b/apps/dokploy/components/dashboard/application/advanced/ports/update-port.tsx deleted file mode 100644 index a9f7f32d5..000000000 --- a/apps/dokploy/components/dashboard/application/advanced/ports/update-port.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input, NumberInput } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, Pencil } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const UpdatePortSchema = z.object({ - publishedPort: z.number().int().min(1).max(65535), - targetPort: z.number().int().min(1).max(65535), - protocol: z.enum(["tcp", "udp"], { - required_error: "Protocol is required", - invalid_type_error: "Protocol must be a valid protocol", - }), -}); - -type UpdatePort = z.infer; - -interface Props { - portId: string; -} - -export const UpdatePort = ({ portId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const utils = api.useUtils(); - const { data } = api.port.one.useQuery( - { - portId, - }, - { - enabled: !!portId, - }, - ); - - const { mutateAsync, isLoading, error, isError } = - api.port.update.useMutation(); - - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(UpdatePortSchema), - }); - - useEffect(() => { - if (data) { - form.reset({ - publishedPort: data.publishedPort, - targetPort: data.targetPort, - protocol: data.protocol, - }); - } - }, [form, form.reset, data]); - - const onSubmit = async (data: UpdatePort) => { - await mutateAsync({ - portId, - publishedPort: data.publishedPort, - targetPort: data.targetPort, - protocol: data.protocol, - }) - .then(async (response) => { - toast.success("Port Updated"); - await utils.application.one.invalidate({ - applicationId: response?.applicationId, - }); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error to update the port"); - }); - }; - - return ( - - - - - - - Update - Update the port - - {isError && {error?.message}} - -
- -
- ( - - Published Port - - - - - - - )} - /> - - ( - - Target Port - - - - - - - )} - /> - { - return ( - - Protocol - - - - ); - }} - /> -
-
- - - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/delete-redirect.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/delete-redirect.tsx deleted file mode 100644 index 08d1f3e03..000000000 --- a/apps/dokploy/components/dashboard/application/advanced/redirects/delete-redirect.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { TrashIcon } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - redirectId: string; -} - -export const DeleteRedirect = ({ redirectId }: Props) => { - const utils = api.useUtils(); - const { mutateAsync, isLoading } = api.redirects.delete.useMutation(); - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - redirect - - - - Cancel - { - await mutateAsync({ - redirectId, - }) - .then((data) => { - utils.application.one.invalidate({ - applicationId: data?.applicationId, - }); - utils.application.readTraefikConfig.invalidate({ - applicationId: data?.applicationId, - }); - toast.success("Redirect delete succesfully"); - }) - .catch(() => { - toast.error("Error to delete the redirect"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/add-redirect.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx similarity index 83% rename from apps/dokploy/components/dashboard/application/advanced/redirects/add-redirect.tsx rename to apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx index 8ce547a29..5d91d580d 100644 --- a/apps/dokploy/components/dashboard/application/advanced/redirects/add-redirect.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx @@ -31,7 +31,7 @@ import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PlusIcon } from "lucide-react"; +import { PenBoxIcon, PlusIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -77,19 +77,32 @@ const redirectPresets = [ interface Props { applicationId: string; + redirectId?: string; children?: React.ReactNode; } -export const AddRedirect = ({ +export const HandleRedirect = ({ applicationId, + redirectId, children = , }: Props) => { const [isOpen, setIsOpen] = useState(false); const [presetSelected, setPresetSelected] = useState(""); + + const { data, refetch } = api.redirects.one.useQuery( + { + redirectId: redirectId || "", + }, + { + enabled: !!redirectId, + }, + ); + const utils = api.useUtils(); - const { mutateAsync, isLoading, error, isError } = - api.redirects.create.useMutation(); + const { mutateAsync, isLoading, error, isError } = redirectId + ? api.redirects.update.useMutation() + : api.redirects.create.useMutation(); const form = useForm({ defaultValues: { @@ -102,29 +115,35 @@ export const AddRedirect = ({ useEffect(() => { form.reset({ - permanent: false, - regex: "", - replacement: "", + permanent: data?.permanent || false, + regex: data?.regex || "", + replacement: data?.replacement || "", }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); + }, [form, form.reset, form.formState.isSubmitSuccessful, data]); const onSubmit = async (data: AddRedirect) => { await mutateAsync({ applicationId, ...data, + redirectId: redirectId || "", }) .then(async () => { - toast.success("Redirect Created"); + toast.success(redirectId ? "Redirect Updated" : "Redirect Created"); await utils.application.one.invalidate({ applicationId, }); + refetch(); await utils.application.readTraefikConfig.invalidate({ applicationId, }); onDialogToggle(false); }) .catch(() => { - toast.error("Error to create the redirect"); + toast.error( + redirectId + ? "Error updating the redirect" + : "Error creating the redirect", + ); }); }; @@ -148,7 +167,17 @@ export const AddRedirect = ({ return ( - + {redirectId ? ( + + ) : ( + + )} @@ -243,7 +272,7 @@ export const AddRedirect = ({ form="hook-form-add-redirect" type="submit" > - Create + {redirectId ? "Update" : "Create"} diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx index 9a8325fc8..4ee597917 100644 --- a/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/redirects/show-redirects.tsx @@ -1,3 +1,5 @@ +import { DialogAction } from "@/components/shared/dialog-action"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -6,23 +8,28 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Split } from "lucide-react"; +import { Split, Trash2 } from "lucide-react"; import React from "react"; -import { AddRedirect } from "./add-redirect"; -import { DeleteRedirect } from "./delete-redirect"; -import { UpdateRedirect } from "./update-redirect"; +import { toast } from "sonner"; +import { HandleRedirect } from "./handle-redirect"; + interface Props { applicationId: string; } export const ShowRedirects = ({ applicationId }: Props) => { - const { data } = api.application.one.useQuery( + const { data, refetch } = api.application.one.useQuery( { applicationId, }, { enabled: !!applicationId }, ); + const { mutateAsync: deleteRedirect, isLoading: isRemoving } = + api.redirects.delete.useMutation(); + + const utils = api.useUtils(); + return ( @@ -35,7 +42,9 @@ export const ShowRedirects = ({ applicationId }: Props) => { {data && data?.redirects.length > 0 && ( - Add Redirect + + Add Redirect + )} @@ -45,9 +54,9 @@ export const ShowRedirects = ({ applicationId }: Props) => { No redirects configured - + Add Redirect - + ) : (
@@ -76,8 +85,40 @@ export const ShowRedirects = ({ applicationId }: Props) => {
- - + + + { + await deleteRedirect({ + redirectId: redirect.redirectId, + }) + .then(() => { + refetch(); + utils.application.readTraefikConfig.invalidate({ + applicationId, + }); + toast.success("Redirect deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting redirect"); + }); + }} + > + +
diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/update-redirect.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/update-redirect.tsx deleted file mode 100644 index 52ff310d2..000000000 --- a/apps/dokploy/components/dashboard/application/advanced/redirects/update-redirect.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Switch } from "@/components/ui/switch"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, Pencil } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -const UpdateRedirectSchema = z.object({ - regex: z.string().min(1, "Regex required"), - permanent: z.boolean().default(false), - replacement: z.string().min(1, "Replacement required"), -}); - -type UpdateRedirect = z.infer; - -interface Props { - redirectId: string; -} - -export const UpdateRedirect = ({ redirectId }: Props) => { - const utils = api.useUtils(); - const [isOpen, setIsOpen] = useState(false); - const { data } = api.redirects.one.useQuery( - { - redirectId, - }, - { - enabled: !!redirectId, - }, - ); - - const { mutateAsync, isLoading, error, isError } = - api.redirects.update.useMutation(); - - const form = useForm({ - defaultValues: { - permanent: false, - regex: "", - replacement: "", - }, - resolver: zodResolver(UpdateRedirectSchema), - }); - - useEffect(() => { - if (data) { - form.reset({ - permanent: data.permanent || false, - regex: data.regex || "", - replacement: data.replacement || "", - }); - } - }, [form, form.reset, data]); - - const onSubmit = async (data: UpdateRedirect) => { - await mutateAsync({ - redirectId, - permanent: data.permanent, - regex: data.regex, - replacement: data.replacement, - }) - .then(async (response) => { - toast.success("Redirect Updated"); - await utils.application.one.invalidate({ - applicationId: response?.applicationId, - }); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error to update the redirect"); - }); - }; - - return ( - - - - - - - Update - Update the redirect - - {isError && {error?.message}} - -
- -
- ( - - Regex - - - - - - - )} - /> - ( - - Replacement - - - - - - - )} - /> - - ( - -
- Permanent - - Set the permanent option to true to apply a permanent - redirection. - -
- - - -
- )} - /> -
-
- - - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/security/delete-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/delete-security.tsx deleted file mode 100644 index 1a63c2345..000000000 --- a/apps/dokploy/components/dashboard/application/advanced/security/delete-security.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { TrashIcon } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - securityId: string; -} - -export const DeleteSecurity = ({ securityId }: Props) => { - const utils = api.useUtils(); - const { mutateAsync, isLoading } = api.security.delete.useMutation(); - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - security - - - - Cancel - { - await mutateAsync({ - securityId, - }) - .then((data) => { - utils.application.one.invalidate({ - applicationId: data?.applicationId, - }); - utils.application.readTraefikConfig.invalidate({ - applicationId: data?.applicationId, - }); - toast.success("Security delete succesfully"); - }) - .catch(() => { - toast.error("Error to delete the security"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/security/add-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx similarity index 72% rename from apps/dokploy/components/dashboard/application/advanced/security/add-security.tsx rename to apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx index aeaebb923..e7bc0cd1f 100644 --- a/apps/dokploy/components/dashboard/application/advanced/security/add-security.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/security/handle-security.tsx @@ -20,7 +20,7 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PlusIcon } from "lucide-react"; +import { PenBoxIcon, PlusIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -35,17 +35,29 @@ type AddSecurity = z.infer; interface Props { applicationId: string; + securityId?: string; children?: React.ReactNode; } -export const AddSecurity = ({ +export const HandleSecurity = ({ applicationId, + securityId, children = , }: Props) => { const utils = api.useUtils(); const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading, error, isError } = - api.security.create.useMutation(); + const { data } = api.security.one.useQuery( + { + securityId: securityId ?? "", + }, + { + enabled: !!securityId, + }, + ); + + const { mutateAsync, isLoading, error, isError } = securityId + ? api.security.update.useMutation() + : api.security.create.useMutation(); const form = useForm({ defaultValues: { @@ -56,16 +68,20 @@ export const AddSecurity = ({ }); useEffect(() => { - form.reset(); - }, [form, form.reset, form.formState.isSubmitSuccessful]); + form.reset({ + username: data?.username || "", + password: data?.password || "", + }); + }, [form, form.reset, form.formState.isSubmitSuccessful, data]); const onSubmit = async (data: AddSecurity) => { await mutateAsync({ applicationId, ...data, + securityId: securityId || "", }) .then(async () => { - toast.success("Security Created"); + toast.success(securityId ? "Security Updated" : "Security Created"); await utils.application.one.invalidate({ applicationId, }); @@ -75,20 +91,34 @@ export const AddSecurity = ({ setIsOpen(false); }) .catch(() => { - toast.error("Error to create the security"); + toast.error( + securityId + ? "Error updating the security" + : "Error creating security", + ); }); }; return ( - + {securityId ? ( + + ) : ( + + )} Security - Add security to your application + {securityId ? "Update" : "Add"} security to your application {isError && {error?.message}} @@ -137,7 +167,7 @@ export const AddSecurity = ({ form="hook-form-add-security" type="submit" > - Create + {securityId ? "Update" : "Create"} diff --git a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx index 5c02bf76d..33022c097 100644 --- a/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/security/show-security.tsx @@ -1,3 +1,5 @@ +import { DialogAction } from "@/components/shared/dialog-action"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -6,23 +8,27 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { LockKeyhole } from "lucide-react"; +import { LockKeyhole, Trash2 } from "lucide-react"; import React from "react"; -import { AddSecurity } from "./add-security"; -import { DeleteSecurity } from "./delete-security"; -import { UpdateSecurity } from "./update-security"; +import { toast } from "sonner"; +import { HandleSecurity } from "./handle-security"; + interface Props { applicationId: string; } export const ShowSecurity = ({ applicationId }: Props) => { - const { data } = api.application.one.useQuery( + const { data, refetch } = api.application.one.useQuery( { applicationId, }, { enabled: !!applicationId }, ); + const { mutateAsync: deleteSecurity, isLoading: isRemoving } = + api.security.delete.useMutation(); + + const utils = api.useUtils(); return ( @@ -32,7 +38,9 @@ export const ShowSecurity = ({ applicationId }: Props) => { {data && data?.security.length > 0 && ( - Add Security + + Add Security + )} @@ -42,9 +50,9 @@ export const ShowSecurity = ({ applicationId }: Props) => { No security configured - + Add Security - + ) : (
@@ -67,8 +75,39 @@ export const ShowSecurity = ({ applicationId }: Props) => {
- - + + { + await deleteSecurity({ + securityId: security.securityId, + }) + .then(() => { + refetch(); + utils.application.readTraefikConfig.invalidate({ + applicationId, + }); + toast.success("Security deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting security"); + }); + }} + > + +
diff --git a/apps/dokploy/components/dashboard/application/advanced/security/update-security.tsx b/apps/dokploy/components/dashboard/application/advanced/security/update-security.tsx deleted file mode 100644 index 1e5af95ff..000000000 --- a/apps/dokploy/components/dashboard/application/advanced/security/update-security.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { PenBoxIcon, Pencil } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const UpdateSecuritySchema = z.object({ - username: z.string().min(1, "Username is required"), - password: z.string().min(1, "Password is required"), -}); - -type UpdateSecurity = z.infer; - -interface Props { - securityId: string; -} - -export const UpdateSecurity = ({ securityId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const utils = api.useUtils(); - const { data } = api.security.one.useQuery( - { - securityId, - }, - { - enabled: !!securityId, - }, - ); - - const { mutateAsync, isLoading, error, isError } = - api.security.update.useMutation(); - - const form = useForm({ - defaultValues: { - username: "", - password: "", - }, - resolver: zodResolver(UpdateSecuritySchema), - }); - - useEffect(() => { - if (data) { - form.reset({ - username: data.username || "", - password: data.password || "", - }); - } - }, [form, form.reset, data]); - - const onSubmit = async (data: UpdateSecurity) => { - await mutateAsync({ - securityId, - username: data.username, - password: data.password, - }) - .then(async (response) => { - toast.success("Security Updated"); - await utils.application.one.invalidate({ - applicationId: response?.applicationId, - }); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error to update the security"); - }); - }; - - return ( - - - - - - - Update - Update the security - - {isError && {error?.message}} - -
- -
- ( - - Username - - - - - - - )} - /> - ( - - Password - - - - - - - )} - /> -
-
- - - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/show-application-advanced-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/show-application-advanced-settings.tsx deleted file mode 100644 index e77be2d10..000000000 --- a/apps/dokploy/components/dashboard/application/advanced/show-application-advanced-settings.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addResourcesApplication = z.object({ - memoryReservation: z.number().nullable().optional(), - cpuLimit: z.number().nullable().optional(), - memoryLimit: z.number().nullable().optional(), - cpuReservation: z.number().nullable().optional(), -}); -interface Props { - applicationId: string; -} - -type AddResourcesApplication = z.infer; - -export const ShowApplicationResources = ({ applicationId }: Props) => { - const { data, refetch } = api.application.one.useQuery( - { - applicationId, - }, - { enabled: !!applicationId }, - ); - const { mutateAsync, isLoading } = api.application.update.useMutation(); - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(addResourcesApplication), - }); - - useEffect(() => { - if (data) { - form.reset({ - cpuLimit: data?.cpuLimit || undefined, - cpuReservation: data?.cpuReservation || undefined, - memoryLimit: data?.memoryLimit || undefined, - memoryReservation: data?.memoryReservation || undefined, - }); - } - }, [data, form, form.reset]); - - const onSubmit = async (formData: AddResourcesApplication) => { - await mutateAsync({ - applicationId, - cpuLimit: formData.cpuLimit || null, - cpuReservation: formData.cpuReservation || null, - memoryLimit: formData.memoryLimit || null, - memoryReservation: formData.memoryReservation || null, - }) - .then(async () => { - toast.success("Resources Updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error to Update the resources"); - }); - }; - return ( - - - Resources - - If you want to decrease or increase the resources to a specific. - application or database - - - - - Please remember to click Redeploy after modify the resources to apply - the changes. - -
- -
- ( - - Memory Reservation - - { - const value = e.target.value; - if (value === "") { - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - field.onChange(number); - } - } - }} - /> - - - - - )} - /> - - { - return ( - - Memory Limit - - { - const value = e.target.value; - if (value === "") { - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - - { - return ( - - Cpu Limit - - { - const value = e.target.value; - if ( - value === "" || - /^[0-9]*\.?[0-9]*$/.test(value) - ) { - const float = Number.parseFloat(value); - field.onChange(float); - } - }} - /> - - - - ); - }} - /> - { - return ( - - Cpu Reservation - - { - const value = e.target.value; - if ( - value === "" || - /^[0-9]*\.?[0-9]*$/.test(value) - ) { - const float = Number.parseFloat(value); - field.onChange(float); - } - }} - /> - - - - ); - }} - /> -
-
- -
-
- -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx new file mode 100644 index 000000000..227bca559 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/show-resources.tsx @@ -0,0 +1,287 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { InfoIcon } from "lucide-react"; +import React, { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const addResourcesSchema = z.object({ + memoryReservation: z.string().optional(), + cpuLimit: z.string().optional(), + memoryLimit: z.string().optional(), + cpuReservation: z.string().optional(), +}); + +export type ServiceType = + | "postgres" + | "mongo" + | "redis" + | "mysql" + | "mariadb" + | "application"; + +interface Props { + id: string; + type: ServiceType | "application"; +} + +type AddResources = z.infer; +export const ShowResources = ({ id, type }: Props) => { + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + defaultValues: { + cpuLimit: "", + cpuReservation: "", + memoryLimit: "", + memoryReservation: "", + }, + resolver: zodResolver(addResourcesSchema), + }); + + useEffect(() => { + if (data) { + form.reset({ + cpuLimit: data?.cpuLimit || undefined, + cpuReservation: data?.cpuReservation || undefined, + memoryLimit: data?.memoryLimit || undefined, + memoryReservation: data?.memoryReservation || undefined, + }); + } + }, [data, form, form.reset]); + + const onSubmit = async (formData: AddResources) => { + await mutateAsync({ + mongoId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + applicationId: id || "", + cpuLimit: formData.cpuLimit || null, + cpuReservation: formData.cpuReservation || null, + memoryLimit: formData.memoryLimit || null, + memoryReservation: formData.memoryReservation || null, + }) + .then(async () => { + toast.success("Resources Updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error updating the resources"); + }); + }; + + return ( + + + Resources + + If you want to decrease or increase the resources to a specific. + application or database + + + + + Please remember to click Redeploy after modify the resources to apply + the changes. + +
+ +
+ { + return ( + +
+ Memory Limit + + + + + + +

+ Memory hard limit in bytes. Example: 1GB = + 1073741824 bytes +

+
+
+
+
+ + + + +
+ ); + }} + /> + ( + +
+ Memory Reservation + + + + + + +

+ Memory soft limit in bytes. Example: 256MB = + 268435456 bytes +

+
+
+
+
+ + + + +
+ )} + /> + + { + return ( + +
+ CPU Limit + + + + + + +

+ CPU quota in units of 10^-9 CPUs. Example: 2 + CPUs = 2000000000 +

+
+
+
+
+ + + + +
+ ); + }} + /> + { + return ( + +
+ CPU Reservation + + + + + + +

+ CPU shares (relative weight). Example: 1 CPU = + 1000000000 +

+
+
+
+
+ + + + +
+ ); + }} + /> +
+
+ +
+
+ +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx index 2c2437a1a..f563f1ab4 100644 --- a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx @@ -105,7 +105,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => { form.reset(); }) .catch(() => { - toast.error("Error to update the traefik config"); + toast.error("Error updating the Traefik config"); }); }; diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx index 5c6b95ca1..718f98b72 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx @@ -107,7 +107,7 @@ export const AddVolumes = ({ setIsOpen(false); }) .catch(() => { - toast.error("Error to create the Bind mount"); + toast.error("Error creating the Bind mount"); }); } else if (data.type === "volume") { await mutateAsync({ @@ -122,7 +122,7 @@ export const AddVolumes = ({ setIsOpen(false); }) .catch(() => { - toast.error("Error to create the Volume mount"); + toast.error("Error creating the Volume mount"); }); } else if (data.type === "file") { await mutateAsync({ @@ -138,7 +138,7 @@ export const AddVolumes = ({ setIsOpen(false); }) .catch(() => { - toast.error("Error to create the File mount"); + toast.error("Error creating the File mount"); }); } diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/delete-volume.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/delete-volume.tsx deleted file mode 100644 index 020a6c36a..000000000 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/delete-volume.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { TrashIcon } from "lucide-react"; -import React from "react"; -import { toast } from "sonner"; - -interface Props { - mountId: string; - refetch: () => void; -} -export const DeleteVolume = ({ mountId, refetch }: Props) => { - const { mutateAsync, isLoading } = api.mounts.remove.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the mount - - - - Cancel - { - await mutateAsync({ - mountId, - }) - .then(() => { - refetch(); - toast.success("Mount deleted succesfully"); - }) - .catch(() => { - toast.error("Error to delete the mount"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index c24d87812..e0f842ce3 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -1,4 +1,6 @@ import { AlertBlock } from "@/components/shared/alert-block"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -7,40 +9,49 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Package } from "lucide-react"; +import { Package, Trash2 } from "lucide-react"; import React from "react"; +import { toast } from "sonner"; +import type { ServiceType } from "../show-resources"; import { AddVolumes } from "./add-volumes"; -import { DeleteVolume } from "./delete-volume"; import { UpdateVolume } from "./update-volume"; interface Props { - applicationId: string; + id: string; + type: ServiceType | "compose"; } -export const ShowVolumes = ({ applicationId }: Props) => { - const { data, refetch } = api.application.one.useQuery( - { - applicationId, - }, - { enabled: !!applicationId }, - ); - +export const ShowVolumes = ({ id, type }: Props) => { + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + compose: () => + api.compose.one.useQuery({ composeId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + const { mutateAsync: deleteVolume, isLoading: isRemoving } = + api.mounts.remove.useMutation(); return (
Volumes - If you want to persist data in this application use the following - config to setup the volumes + If you want to persist data in this service use the following config + to setup the volumes
{data && data?.mounts.length > 0 && ( - + Add Volume )} @@ -52,17 +63,13 @@ export const ShowVolumes = ({ applicationId }: Props) => { No volumes/mounts configured - + Add Volume ) : (
- + Please remember to click Redeploy after adding, editing, or deleting a mount to apply the changes. @@ -73,7 +80,8 @@ export const ShowVolumes = ({ applicationId }: Props) => { key={mount.mountId} className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4" > -
+ {/* */} +
Mount Type @@ -90,21 +98,12 @@ export const ShowVolumes = ({ applicationId }: Props) => { )} {mount.type === "file" && ( - <> -
- Content - - {mount.content} - -
- -
- File Path - - {mount.filePath} - -
- +
+ Content + + {mount.content} + +
)} {mount.type === "bind" && (
@@ -114,21 +113,55 @@ export const ShowVolumes = ({ applicationId }: Props) => {
)} -
- Mount Path - - {mount.mountPath} - -
+ {mount.type === "file" ? ( +
+ File Path + + {mount.filePath} + +
+ ) : ( +
+ Mount Path + + {mount.mountPath} + +
+ )}
- + { + await deleteVolume({ + mountId: mount.mountId, + }) + .then(() => { + refetch(); + toast.success("Volume deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting volume"); + }); + }} + > + +
diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx index 80cc97ad0..d8481d652 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/update-volume.tsx @@ -21,7 +21,7 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Pencil } from "lucide-react"; +import { PenBoxIcon, Pencil } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -139,7 +139,7 @@ export const UpdateVolume = ({ setIsOpen(false); }) .catch(() => { - toast.error("Error to update the Bind mount"); + toast.error("Error updating the Bind mount"); }); } else if (data.type === "volume") { await mutateAsync({ @@ -153,7 +153,7 @@ export const UpdateVolume = ({ setIsOpen(false); }) .catch(() => { - toast.error("Error to update the Volume mount"); + toast.error("Error updating the Volume mount"); }); } else if (data.type === "file") { await mutateAsync({ @@ -168,7 +168,7 @@ export const UpdateVolume = ({ setIsOpen(false); }) .catch(() => { - toast.error("Error to update the File mount"); + toast.error("Error updating the File mount"); }); } refetch(); @@ -177,8 +177,13 @@ export const UpdateVolume = ({ return ( - diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index edfb38ae8..ad83f456c 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -126,7 +126,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the build type"); + toast.error("Error saving the build type"); }); }; diff --git a/apps/dokploy/components/dashboard/application/delete-application.tsx b/apps/dokploy/components/dashboard/application/delete-application.tsx deleted file mode 100644 index 93173d637..000000000 --- a/apps/dokploy/components/dashboard/application/delete-application.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const deleteApplicationSchema = z.object({ - projectName: z.string().min(1, { - message: "Application name is required", - }), -}); - -type DeleteApplication = z.infer; - -interface Props { - applicationId: string; -} - -export const DeleteApplication = ({ applicationId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.application.delete.useMutation(); - const { data } = api.application.one.useQuery( - { applicationId }, - { enabled: !!applicationId }, - ); - const { push } = useRouter(); - const form = useForm({ - defaultValues: { - projectName: "", - }, - resolver: zodResolver(deleteApplicationSchema), - }); - - const onSubmit = async (formData: DeleteApplication) => { - const expectedName = `${data?.name}/${data?.appName}`; - if (formData.projectName === expectedName) { - await mutateAsync({ - applicationId, - }) - .then((data) => { - push(`/dashboard/project/${data?.projectId}`); - toast.success("Application deleted successfully"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error deleting the application"); - }); - } else { - form.setError("projectName", { - message: "Project name does not match", - }); - } - }; - - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - application. If you are sure please enter the application name to - delete this application. - - -
-
- - ( - - - To confirm, type "{data?.name}/{data?.appName}" in the box - below - - - - - - - )} - /> - - -
- - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx index 110e168f5..5fe7ffb07 100644 --- a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx @@ -20,6 +20,12 @@ interface Props { export const CancelQueues = ({ applicationId }: Props) => { const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + + if (isCloud) { + return null; + } + return ( diff --git a/apps/dokploy/components/dashboard/application/deployments/refresh-token.tsx b/apps/dokploy/components/dashboard/application/deployments/refresh-token.tsx index 88d5beee9..c268e6d51 100644 --- a/apps/dokploy/components/dashboard/application/deployments/refresh-token.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/refresh-token.tsx @@ -47,7 +47,7 @@ export const RefreshToken = ({ applicationId }: Props) => { toast.success("Refresh updated"); }) .catch(() => { - toast.error("Error to update the refresh token"); + toast.error("Error updating the refresh token"); }); }} > diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 8c15e2cde..e6fdb38be 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -1,3 +1,5 @@ +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -5,18 +7,45 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Loader2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { TerminalLine } from "../../docker/logs/terminal-line"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; interface Props { logPath: string | null; open: boolean; onClose: () => void; serverId?: string; + errorMessage?: string; } -export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { +export const ShowDeployment = ({ + logPath, + open, + onClose, + serverId, + errorMessage, +}: Props) => { const [data, setData] = useState(""); - const endOfLogsRef = useRef(null); + const [showExtraLogs, setShowExtraLogs] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); const wsRef = useRef(null); // Ref to hold WebSocket instance + const [autoScroll, setAutoScroll] = useState(true); + const scrollRef = useRef(null); + + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; + + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + setAutoScroll(isAtBottom); + }; useEffect(() => { if (!open || !logPath) return; @@ -48,13 +77,36 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { }; }, [logPath, open]); - const scrollToBottom = () => { - endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" }); - }; + useEffect(() => { + const logs = parseLogs(data); + let filteredLogsResult = logs; + if (serverId) { + let hideSubsequentLogs = false; + filteredLogsResult = logs.filter((log) => { + if ( + log.message.includes( + "===================================EXTRA LOGS============================================", + ) + ) { + hideSubsequentLogs = true; + return showExtraLogs; + } + return showExtraLogs ? true : !hideSubsequentLogs; + }); + } + + setFilteredLogs(filteredLogsResult); + }, [data, showExtraLogs]); useEffect(() => { scrollToBottom(); - }, [data]); + + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [filteredLogs, autoScroll]); + + const optionalErrors = parseLogs(errorMessage || ""); return ( { Deployment - - See all the details of this deployment + + + See all the details of this deployment |{" "} + + {filteredLogs.length} lines + + + + {serverId && ( +
+ + setShowExtraLogs(checked as boolean) + } + /> + +
+ )}
-
- -
-							{data || "Loading..."}
-						
-
- +
+ {" "} + {filteredLogs.length > 0 ? ( + filteredLogs.map((log: LogLine, index: number) => ( + + )) + ) : ( + <> + {optionalErrors.length > 0 ? ( + optionalErrors.map((log: LogLine, index: number) => ( + + )) + ) : ( +
+ +
+ )} + + )}
diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index a767350f9..d33936f5d 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -8,7 +8,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { api } from "@/utils/api"; +import { type RouterOutputs, api } from "@/utils/api"; import { RocketIcon } from "lucide-react"; import React, { useEffect, useState } from "react"; import { CancelQueues } from "./cancel-queues"; @@ -18,8 +18,11 @@ import { ShowDeployment } from "./show-deployment"; interface Props { applicationId: string; } + export const ShowDeployments = ({ applicationId }: Props) => { - const [activeLog, setActiveLog] = useState(null); + const [activeLog, setActiveLog] = useState< + RouterOutputs["deployment"]["all"][number] | null + >(null); const { data } = api.application.one.useQuery({ applicationId }); const { data: deployments } = api.deployment.all.useQuery( { applicationId }, @@ -100,7 +103,7 @@ export const ShowDeployments = ({ applicationId }: Props) => { -
- - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - domain - - - - Cancel - { - await mutateAsync({ - domainId, - }) - .then((data) => { - if (data?.applicationId) { - utils.domain.byApplicationId.invalidate({ - applicationId: data?.applicationId, - }); - utils.application.readTraefikConfig.invalidate({ - applicationId: data?.applicationId, - }); - } else if (data?.composeId) { - utils.domain.byComposeId.invalidate({ - composeId: data?.composeId, - }); - } - - toast.success("Domain delete succesfully"); - }) - .catch(() => { - toast.error("Error to delete Domain"); - }); - }} - > - Confirm - - - -
- ); -}; diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 8ca59061e..21b7a9f50 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -1,3 +1,4 @@ +import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; import { Card, @@ -8,17 +9,17 @@ import { } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react"; +import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react"; import Link from "next/link"; +import { toast } from "sonner"; import { AddDomain } from "./add-domain"; -import { DeleteDomain } from "./delete-domain"; interface Props { applicationId: string; } export const ShowDomains = ({ applicationId }: Props) => { - const { data } = api.domain.byApplicationId.useQuery( + const { data, refetch } = api.domain.byApplicationId.useQuery( { applicationId, }, @@ -26,6 +27,10 @@ export const ShowDomains = ({ applicationId }: Props) => { enabled: !!applicationId, }, ); + + const { mutateAsync: deleteDomain, isLoading: isRemoving } = + api.domain.delete.useMutation(); + return (
@@ -93,11 +98,40 @@ export const ShowDomains = ({ applicationId }: Props) => { applicationId={applicationId} domainId={item.domainId} > - - + { + await deleteDomain({ + domainId: item.domainId, + }) + .then((data) => { + refetch(); + toast.success("Domain deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting domain"); + }); + }} + > + +
); diff --git a/apps/dokploy/components/dashboard/mariadb/environment/show-mariadb-environment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx similarity index 63% rename from apps/dokploy/components/dashboard/mariadb/environment/show-mariadb-environment.tsx rename to apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx index 61f64cd8c..b65a18161 100644 --- a/apps/dokploy/components/dashboard/mariadb/environment/show-mariadb-environment.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx @@ -18,10 +18,11 @@ import { Toggle } from "@/components/ui/toggle"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { EyeIcon, EyeOffIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import React, { type CSSProperties, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import type { ServiceType } from "../advanced/show-resources"; const addEnvironmentSchema = z.object({ environment: z.string(), @@ -30,21 +31,39 @@ const addEnvironmentSchema = z.object({ type EnvironmentSchema = z.infer; interface Props { - mariadbId: string; + id: string; + type: Exclude; } -export const ShowMariadbEnvironment = ({ mariadbId }: Props) => { +export const ShowEnvironment = ({ id, type }: Props) => { + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + compose: () => + api.compose.one.useQuery({ composeId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); const [isEnvVisible, setIsEnvVisible] = useState(true); - const { mutateAsync, isLoading } = api.mariadb.saveEnvironment.useMutation(); - const { data, refetch } = api.mariadb.one.useQuery( - { - mariadbId, - }, - { - enabled: !!mariadbId, - }, - ); + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + compose: () => api.compose.update.useMutation(), + }; + const { mutateAsync, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + const form = useForm({ defaultValues: { environment: "", @@ -62,22 +81,26 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => { const onSubmit = async (data: EnvironmentSchema) => { mutateAsync({ + mongoId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + composeId: id || "", env: data.environment, - mariadbId, }) .then(async () => { toast.success("Environments Added"); await refetch(); }) .catch(() => { - toast.error("Error to add environment"); + toast.error("Error adding environment"); }); }; return (
- {" "}
Environment Settings @@ -112,6 +135,11 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => { { await refetch(); }) .catch(() => { - toast.error("Error to add environment"); + toast.error("Error adding environment"); }); }; return ( -
- - + + + { placeholder="NPM_TOKEN=xyz" /> )} - -
- -
-
-
- - +
+ +
+ + +
); }; diff --git a/apps/dokploy/components/dashboard/application/general/deploy-application.tsx b/apps/dokploy/components/dashboard/application/general/deploy-application.tsx deleted file mode 100644 index f9115c769..000000000 --- a/apps/dokploy/components/dashboard/application/general/deploy-application.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { toast } from "sonner"; - -interface Props { - applicationId: string; -} - -export const DeployApplication = ({ applicationId }: Props) => { - const { data, refetch } = api.application.one.useQuery( - { - applicationId, - }, - { enabled: !!applicationId }, - ); - - const { mutateAsync: deploy } = api.application.deploy.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will deploy the application - - - - Cancel - { - await deploy({ - applicationId, - }) - .then(async () => { - toast.success("Application deployed succesfully"); - await refetch(); - }) - - .catch(() => { - toast.error("Error to deploy Application"); - }); - - await refetch(); - }} - > - Confirm - - - - - ); -}; 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 d6dfa20e7..9b207d636 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 @@ -137,7 +137,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the Bitbucket provider"); + toast.error("Error saving the Bitbucket provider"); }); }; @@ -235,7 +235,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -245,7 +245,12 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.username} + + { await refetch(); }) .catch(() => { - toast.error("Error to save the Docker provider"); + toast.error("Error saving the Docker provider"); }); }; diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx index 4ed9df163..3732860d4 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx @@ -56,7 +56,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the deployment"); + toast.error("Error saving the deployment"); }); }; 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 58fd1973a..218e004d7 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 @@ -84,7 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the Git provider"); + toast.error("Error saving the Git provider"); }); }; 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 5c432e6af..adb445752 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 @@ -131,7 +131,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the github provider"); + toast.error("Error saving the github provider"); }); }; @@ -226,7 +226,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -236,7 +236,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.login} + + { await refetch(); }) .catch(() => { - toast.error("Error to save the gitlab provider"); + toast.error("Error saving the gitlab provider"); }); }; @@ -248,7 +248,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { {repositories?.map((repo) => { return ( { form.setValue("repository", { @@ -260,7 +260,12 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.username} + + { - const { refetch } = api.application.one.useQuery( - { - applicationId, - }, - { enabled: !!applicationId }, - ); - const { mutateAsync: reload, isLoading } = - api.application.reload.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will reload the application - - - - Cancel - { - await reload({ - applicationId, - appName, - }) - .then(() => { - toast.success("Service Reloaded"); - }) - .catch(() => { - toast.error("Error to reload the service"); - }); - await refetch(); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index 65c99e9b8..83e4b6f06 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -1,23 +1,21 @@ import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show"; import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show"; +import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; -import { Terminal } from "lucide-react"; +import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react"; +import { useRouter } from "next/router"; import React from "react"; import { toast } from "sonner"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; -import { RedbuildApplication } from "../rebuild-application"; -import { StartApplication } from "../start-application"; -import { StopApplication } from "../stop-application"; -import { DeployApplication } from "./deploy-application"; -import { ResetApplication } from "./reset-application"; interface Props { applicationId: string; } export const ShowGeneralApplication = ({ applicationId }: Props) => { + const router = useRouter(); const { data, refetch } = api.application.one.useQuery( { applicationId, @@ -25,6 +23,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { { enabled: !!applicationId }, ); const { mutateAsync: update } = api.application.update.useMutation(); + const { mutateAsync: start, isLoading: isStarting } = + api.application.start.useMutation(); + const { mutateAsync: stop, isLoading: isStopping } = + api.application.stop.useMutation(); + + const { mutateAsync: deploy, isLoading: isDeploying } = + api.application.deploy.useMutation(); + + const { mutateAsync: reload, isLoading: isReloading } = + api.application.reload.useMutation(); + + const { mutateAsync: redeploy } = api.application.redeploy.useMutation(); return ( <> @@ -33,17 +43,127 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { Deploy Settings - - + { + await deploy({ + applicationId: applicationId, + }) + .then(() => { + toast.success("Application deployed successfully"); + refetch(); + router.push( + `/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`, + ); + }) + .catch(() => { + toast.error("Error deploying application"); + }); + }} + > + + + { + await reload({ + applicationId: applicationId, + appName: data?.appName || "", + }) + .then(() => { + toast.success("Application reloaded successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error reloading application"); + }); + }} + > + + + { + await redeploy({ + applicationId: applicationId, + }) + .then(() => { + toast.success("Application rebuilt successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error rebuilding application"); + }); + }} + > + + - {data?.applicationStatus === "idle" ? ( - + { + await start({ + applicationId: applicationId, + }) + .then(() => { + toast.success("Application started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting application"); + }); + }} + > + + ) : ( - + { + await stop({ + applicationId: applicationId, + }) + .then(() => { + toast.success("Application stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping application"); + }); + }} + > + + )} { await refetch(); }) .catch(() => { - toast.error("Error to update Auto Deploy"); + toast.error("Error updating Auto Deploy"); }); }} className="flex flex-row gap-2 items-center" diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index 33beab120..a73b99d25 100644 --- a/apps/dokploy/components/dashboard/application/logs/show.tsx +++ b/apps/dokploy/components/dashboard/application/logs/show.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, @@ -15,6 +16,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { Loader2 } from "lucide-react"; import dynamic from "next/dynamic"; @@ -29,28 +31,67 @@ export const DockerLogs = dynamic( }, ); +export const badgeStateColor = (state: string) => { + switch (state) { + case "running": + return "green"; + case "exited": + case "shutdown": + return "red"; + case "accepted": + case "created": + return "blue"; + default: + return "default"; + } +}; + interface Props { appName: string; serverId?: string; } export const ShowDockerLogs = ({ appName, serverId }: Props) => { - const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery( - { - appName, - serverId, - }, - { - enabled: !!appName, - }, - ); const [containerId, setContainerId] = useState(); + const [option, setOption] = useState<"swarm" | "native">("native"); + + const { data: services, isLoading: servicesLoading } = + api.docker.getServiceContainersByAppName.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "swarm", + }, + ); + + const { data: containers, isLoading: containersLoading } = + api.docker.getContainersByAppNameMatch.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "native", + }, + ); useEffect(() => { - if (data && data?.length > 0) { - setContainerId(data[0]?.containerId); + if (option === "native") { + if (containers && containers?.length > 0) { + setContainerId(containers[0]?.containerId); + } + } else { + if (services && services?.length > 0) { + setContainerId(services[0]?.containerId); + } } - }, [data]); + }, [option, services, containers]); + + const isLoading = option === "native" ? containersLoading : servicesLoading; + const containersLenght = + option === "native" ? containers?.length : services?.length; return ( @@ -62,7 +103,21 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => { - +
+ +
+ + {option === "native" ? "Native" : "Swarm"} + + { + setOption(checked ? "native" : "swarm"); + }} + /> +
+
+
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx index e8a259d15..64b7c3c61 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx @@ -104,9 +104,7 @@ export const AddPreviewDomain = ({ const dictionary = { success: domainId ? "Domain Updated" : "Domain Created", - error: domainId - ? "Error to update the domain" - : "Error to create the domain", + error: domainId ? "Error updating the domain" : "Error creating the domain", submit: domainId ? "Update" : "Create", dialogDescription: domainId ? "In this section you can edit a domain" @@ -265,21 +263,21 @@ export const AddPreviewDomain = ({ name="certificateType" render={({ field }) => ( - Certificate + Certificate Provider diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx index 4eb2107f6..55b31f3f0 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx @@ -18,15 +18,28 @@ import { ShowDeployment } from "../deployments/show-deployment"; interface Props { deployments: RouterOutputs["deployment"]["all"]; serverId?: string; + trigger?: React.ReactNode; } -export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => { - const [activeLog, setActiveLog] = useState(null); +export const ShowPreviewBuilds = ({ + deployments, + serverId, + trigger, +}: Props) => { + const [activeLog, setActiveLog] = useState< + RouterOutputs["deployment"]["all"][number] | null + >(null); const [isOpen, setIsOpen] = useState(false); return ( - + {trigger ? ( + trigger + ) : ( + + )} @@ -66,7 +79,7 @@ export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => { ); diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 45451e78f..371276bdd 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -1,5 +1,8 @@ +import { GithubIcon } from "@/components/icons/data-tools-icons"; import { DateTooltip } from "@/components/shared/date-tooltip"; +import { DialogAction } from "@/components/shared/dialog-action"; import { StatusTooltip } from "@/components/shared/status-tooltip"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, @@ -8,30 +11,34 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { Pencil, RocketIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import { + ExternalLink, + FileText, + GitPullRequest, + Layers, + PenSquare, + RocketIcon, + Trash2, +} from "lucide-react"; +import React from "react"; import { toast } from "sonner"; -import { ShowDeployment } from "../deployments/show-deployment"; -import Link from "next/link"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; -import { DialogAction } from "@/components/shared/dialog-action"; import { AddPreviewDomain } from "./add-preview-domain"; -import { GithubIcon } from "@/components/icons/data-tools-icons"; -import { ShowPreviewSettings } from "./show-preview-settings"; import { ShowPreviewBuilds } from "./show-preview-builds"; +import { ShowPreviewSettings } from "./show-preview-settings"; interface Props { applicationId: string; } export const ShowPreviewDeployments = ({ applicationId }: Props) => { - const [activeLog, setActiveLog] = useState(null); const { data } = api.application.one.useQuery({ applicationId }); const { mutateAsync: deletePreviewDeployment, isLoading } = api.previewDeployment.delete.useMutation(); + const { data: previewDeployments, refetch: refetchPreviewDeployments } = api.previewDeployment.all.useQuery( { applicationId }, @@ -39,10 +46,19 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { enabled: !!applicationId, }, ); - // const [url, setUrl] = React.useState(""); - // useEffect(() => { - // setUrl(document.location.origin); - // }, []); + + const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { + deletePreviewDeployment({ + previewDeploymentId: previewDeploymentId, + }) + .then(() => { + refetchPreviewDeployments(); + toast.success("Preview deployment deleted"); + }) + .catch((error) => { + toast.error(error.message); + }); + }; return ( @@ -65,7 +81,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { each pull request you create.
- {data?.previewDeployments?.length === 0 ? ( + {!previewDeployments?.length ? (
@@ -74,120 +90,131 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
) : (
- {previewDeployments?.map((previewDeployment) => { - const { deployments, domain } = previewDeployment; - + {previewDeployments?.map((deployment) => { + const deploymentUrl = `${deployment.domain?.https ? "https" : "http"}://${deployment.domain?.host}${deployment.domain?.path || "/"}`; + const status = deployment.previewStatus; return (
-
-
- {deployments?.length === 0 ? ( -
- - No deployments found - -
- ) : ( -
- - {previewDeployment?.pullRequestTitle} - - -
- )} -
- {previewDeployment?.pullRequestTitle && ( -
- - Title: {previewDeployment?.pullRequestTitle} - -
- )} +
- {previewDeployment?.pullRequestURL && ( -
- - - Pull Request URL - +
+
+
+ +
+
+ {deployment.pullRequestTitle} +
+
+ {deployment.branch}
- )} -
-
- Domain -
- - {domain?.host} - - - -
+ + + +
-
- {previewDeployment?.createdAt && ( -
- -
- )} - +
+
+ + window.open(deploymentUrl, "_blank") + } + /> + +
- - - - - { - deletePreviewDeployment({ - previewDeploymentId: - previewDeployment.previewDeploymentId, - }) - .then(() => { - refetchPreviewDeployments(); - toast.success("Preview deployment deleted"); - }) - .catch((error) => { - toast.error(error.message); - }); - }} - > - - + + + + + + + Builds + + } + /> + + + + + + handleDeletePreviewDeployment( + deployment.previewDeploymentId, + ) + } + > + + +
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index 6e56bbdd0..fec61ca60 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -1,5 +1,3 @@ -import { api } from "@/utils/api"; -import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -20,12 +18,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input, NumberInput } from "@/components/ui/input"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; import { Secrets } from "@/components/ui/secrets"; -import { toast } from "sonner"; -import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, @@ -33,6 +26,14 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Settings2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; const schema = z.object({ env: z.string(), @@ -116,7 +117,10 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
- + @@ -218,21 +222,21 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { name="previewCertificateType" render={({ field }) => ( - Certificate + Certificate Provider @@ -287,16 +291,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { "PORT=3000", ].join("\n")} /> - {/* */} diff --git a/apps/dokploy/components/dashboard/application/rebuild-application.tsx b/apps/dokploy/components/dashboard/application/rebuild-application.tsx deleted file mode 100644 index 0284ab8f6..000000000 --- a/apps/dokploy/components/dashboard/application/rebuild-application.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Hammer } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - applicationId: string; -} - -export const RedbuildApplication = ({ applicationId }: Props) => { - const { data } = api.application.one.useQuery( - { - applicationId, - }, - { enabled: !!applicationId }, - ); - - const { mutateAsync } = api.application.redeploy.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you sure to rebuild the application? - - - Is required to deploy at least 1 time in order to reuse the same - code - - - - Cancel - { - toast.success("Redeploying Application...."); - await mutateAsync({ - applicationId, - }) - .then(async () => { - await utils.application.one.invalidate({ - applicationId, - }); - }) - .catch(() => { - toast.error("Error to rebuild the application"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/start-application.tsx b/apps/dokploy/components/dashboard/application/start-application.tsx deleted file mode 100644 index d65997ed5..000000000 --- a/apps/dokploy/components/dashboard/application/start-application.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { CheckCircle2 } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - applicationId: string; -} - -export const StartApplication = ({ applicationId }: Props) => { - const { mutateAsync, isLoading } = api.application.start.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you sure to start the application? - - - This will start the application - - - - Cancel - { - await mutateAsync({ - applicationId, - }) - .then(async () => { - await utils.application.one.invalidate({ - applicationId, - }); - toast.success("Application started succesfully"); - }) - .catch(() => { - toast.error("Error to start the Application"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/stop-application.tsx b/apps/dokploy/components/dashboard/application/stop-application.tsx deleted file mode 100644 index 5eb873f41..000000000 --- a/apps/dokploy/components/dashboard/application/stop-application.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Ban } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - applicationId: string; -} - -export const StopApplication = ({ applicationId }: Props) => { - const { mutateAsync, isLoading } = api.application.stop.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you absolutely sure to stop the application? - - - This will stop the application - - - - Cancel - { - await mutateAsync({ - applicationId, - }) - .then(async () => { - await utils.application.one.invalidate({ - applicationId, - }); - toast.success("Application stopped succesfully"); - }) - .catch(() => { - toast.error("Error to stop the Application"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/application/update-application.tsx b/apps/dokploy/components/dashboard/application/update-application.tsx index f40a00203..a49fc5383 100644 --- a/apps/dokploy/components/dashboard/application/update-application.tsx +++ b/apps/dokploy/components/dashboard/application/update-application.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, SquarePen } from "lucide-react"; +import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -76,14 +76,14 @@ export const UpdateApplication = ({ applicationId }: Props) => { description: formData.description || "", }) .then(() => { - toast.success("Application updated succesfully"); + toast.success("Application updated successfully"); utils.application.one.invalidate({ applicationId: applicationId, }); setIsOpen(false); }) .catch(() => { - toast.error("Error to update the application"); + toast.error("Error updating the Application"); }) .finally(() => {}); }; @@ -91,8 +91,12 @@ export const UpdateApplication = ({ applicationId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx index 44ce15c04..1bbd38205 100644 --- a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx +++ b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Card, @@ -81,7 +82,7 @@ export const AddCommandCompose = ({ composeId }: Props) => { }); }) .catch(() => { - toast.error("Error to update the command"); + toast.error("Error updating the command"); }); }; @@ -91,7 +92,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
Run Command - Append a custom command to the compose file + Override a custom command to the compose file
@@ -101,6 +102,12 @@ export const AddCommandCompose = ({ composeId }: Props) => { onSubmit={form.handleSubmit(onSubmit)} className="grid w-full gap-4" > + + Modifying the default command may affect deployment stability, + impacting logs and monitoring. Proceed carefully and test + thoroughly. By default, the command starts with{" "} + docker. +
{ - const { data, refetch } = api.compose.one.useQuery( - { - composeId, - }, - { enabled: !!composeId }, - ); - - return ( - - -
- Volumes - - If you want to persist data in this compose use the following config - to setup the volumes - -
- - {data && data?.mounts.length > 0 && ( - - Add Volume - - )} -
- - {data?.mounts.length === 0 ? ( -
- - - No volumes/mounts configured - - - Add Volume - -
- ) : ( -
- - Please remember to click Redeploy after adding, editing, or - deleting a mount to apply the changes. - - -
- {data?.mounts.map((mount) => ( -
-
-
-
- Mount Type - - {mount.type.toUpperCase()} - -
- {mount.type === "volume" && ( -
- Volume Name - - {mount.volumeName} - -
- )} - - {mount.type === "file" && ( - <> -
- Content - - {mount.content} - -
-
- File Path - - {mount.filePath} - -
- - )} - {mount.type === "bind" && ( -
- Host Path - - {mount.hostPath} - -
- )} -
- Mount Path - - {mount.mountPath} - -
-
-
- - -
-
-
- ))} -
-
- )} -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/compose/delete-compose.tsx b/apps/dokploy/components/dashboard/compose/delete-compose.tsx deleted file mode 100644 index 07f42448c..000000000 --- a/apps/dokploy/components/dashboard/compose/delete-compose.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const deleteComposeSchema = z.object({ - projectName: z.string().min(1, { - message: "Compose name is required", - }), -}); - -type DeleteCompose = z.infer; - -interface Props { - composeId: string; -} - -export const DeleteCompose = ({ composeId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.compose.delete.useMutation(); - const { data } = api.compose.one.useQuery( - { composeId }, - { enabled: !!composeId }, - ); - const { push } = useRouter(); - const form = useForm({ - defaultValues: { - projectName: "", - }, - resolver: zodResolver(deleteComposeSchema), - }); - - const onSubmit = async (formData: DeleteCompose) => { - const expectedName = `${data?.name}/${data?.appName}`; - if (formData.projectName === expectedName) { - await mutateAsync({ composeId }) - .then((result) => { - push(`/dashboard/project/${result?.projectId}`); - toast.success("Compose deleted successfully"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error deleting the compose"); - }); - } else { - form.setError("projectName", { - message: `Project name must match "${expectedName}"`, - }); - } - }; - - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - compose. If you are sure please enter the compose name to delete - this compose. - - -
-
- - ( - - - To confirm, type "{data?.name}/{data?.appName}" in the box - below - {" "} - - - - - - )} - /> - - -
- - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/compose/delete-service.tsx b/apps/dokploy/components/dashboard/compose/delete-service.tsx new file mode 100644 index 000000000..00656f6c7 --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/delete-service.tsx @@ -0,0 +1,226 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import type { ServiceType } from "@dokploy/server/db/schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import copy from "copy-to-clipboard"; +import { Copy, Trash2 } from "lucide-react"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const deleteComposeSchema = z.object({ + projectName: z.string().min(1, { + message: "Compose name is required", + }), + deleteVolumes: z.boolean(), +}); + +type DeleteCompose = z.infer; + +interface Props { + id: string; + type: ServiceType | "application"; +} + +export const DeleteService = ({ id, type }: Props) => { + const [isOpen, setIsOpen] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + compose: () => + api.compose.one.useQuery({ composeId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.remove.useMutation(), + redis: () => api.redis.remove.useMutation(), + mysql: () => api.mysql.remove.useMutation(), + mariadb: () => api.mariadb.remove.useMutation(), + application: () => api.application.delete.useMutation(), + mongo: () => api.mongo.remove.useMutation(), + compose: () => api.compose.delete.useMutation(), + }; + const { mutateAsync, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.remove.useMutation(); + const { push } = useRouter(); + const form = useForm({ + defaultValues: { + projectName: "", + deleteVolumes: false, + }, + resolver: zodResolver(deleteComposeSchema), + }); + + const onSubmit = async (formData: DeleteCompose) => { + const expectedName = `${data?.name}/${data?.appName}`; + if (formData.projectName === expectedName) { + const { deleteVolumes } = formData; + await mutateAsync({ + mongoId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + applicationId: id || "", + composeId: id || "", + deleteVolumes, + }) + .then((result) => { + push(`/dashboard/project/${result?.projectId}`); + toast.success("deleted successfully"); + setIsOpen(false); + }) + .catch(() => { + toast.error("Error deleting the service"); + }); + } else { + form.setError("projectName", { + message: `Project name must match "${expectedName}"`, + }); + } + }; + + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the + service. If you are sure please enter the service name to delete + this service. + + +
+
+ + ( + + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + copy(`${data.name}/${data.appName}`); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + + + + + + + + )} + /> + {type === "compose" && ( + ( + +
+ + + + + + Delete volumes associated with this compose + +
+ +
+ )} + /> + )} + + +
+ + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/deployments/cancel-queues-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/cancel-queues-compose.tsx index c84167c4d..a430ae18f 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/cancel-queues-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/cancel-queues-compose.tsx @@ -20,6 +20,11 @@ interface Props { export const CancelQueuesCompose = ({ composeId }: Props) => { const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + + if (isCloud) { + return null; + } return ( diff --git a/apps/dokploy/components/dashboard/compose/deployments/refresh-token-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/refresh-token-compose.tsx index 66d690e7a..95fafaab1 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/refresh-token-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/refresh-token-compose.tsx @@ -47,7 +47,7 @@ export const RefreshTokenCompose = ({ composeId }: Props) => { toast.success("Refresh Token updated"); }) .catch(() => { - toast.error("Error to update the refresh token"); + toast.error("Error updating the refresh token"); }); }} > diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx index 14f3bcd7d..7c191a14c 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx @@ -1,3 +1,5 @@ +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -5,23 +7,45 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Loader2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { TerminalLine } from "../../docker/logs/terminal-line"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; interface Props { logPath: string | null; serverId?: string; open: boolean; onClose: () => void; + errorMessage?: string; } export const ShowDeploymentCompose = ({ logPath, open, onClose, serverId, + errorMessage, }: Props) => { const [data, setData] = useState(""); - const endOfLogsRef = useRef(null); + const [filteredLogs, setFilteredLogs] = useState([]); + const [showExtraLogs, setShowExtraLogs] = useState(false); const wsRef = useRef(null); // Ref to hold WebSocket instance + const [autoScroll, setAutoScroll] = useState(true); + const scrollRef = useRef(null); + + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; + + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + setAutoScroll(isAtBottom); + }; useEffect(() => { if (!open || !logPath) return; @@ -54,13 +78,36 @@ export const ShowDeploymentCompose = ({ }; }, [logPath, open]); - const scrollToBottom = () => { - endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" }); - }; + useEffect(() => { + const logs = parseLogs(data); + let filteredLogsResult = logs; + if (serverId) { + let hideSubsequentLogs = false; + filteredLogsResult = logs.filter((log) => { + if ( + log.message.includes( + "===================================EXTRA LOGS============================================", + ) + ) { + hideSubsequentLogs = true; + return showExtraLogs; + } + return showExtraLogs ? true : !hideSubsequentLogs; + }); + } + + setFilteredLogs(filteredLogsResult); + }, [data, showExtraLogs]); useEffect(() => { scrollToBottom(); - }, [data]); + + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [filteredLogs, autoScroll]); + + const optionalErrors = parseLogs(errorMessage || ""); return ( - + Deployment - - See all the details of this deployment + + + See all the details of this deployment |{" "} + + {filteredLogs.length} lines + + + {serverId && ( +
+ + setShowExtraLogs(checked as boolean) + } + /> + +
+ )}
-
- -
-							{data || "Loading..."}
-						
-
- +
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((log: LogLine, index: number) => ( + + )) + ) : ( + <> + {optionalErrors.length > 0 ? ( + optionalErrors.map((log: LogLine, index: number) => ( + + )) + ) : ( +
+ +
+ )} + + )}
diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployments-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployments-compose.tsx index 54c0ad2e2..fce4f33f9 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployments-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployments-compose.tsx @@ -8,7 +8,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { api } from "@/utils/api"; +import { type RouterOutputs, api } from "@/utils/api"; import { RocketIcon } from "lucide-react"; import React, { useEffect, useState } from "react"; import { CancelQueuesCompose } from "./cancel-queues-compose"; @@ -19,7 +19,9 @@ interface Props { composeId: string; } export const ShowDeploymentsCompose = ({ composeId }: Props) => { - const [activeLog, setActiveLog] = useState(null); + const [activeLog, setActiveLog] = useState< + RouterOutputs["deployment"]["all"][number] | null + >(null); const { data } = api.compose.one.useQuery({ composeId }); const { data: deployments } = api.deployment.allByCompose.useQuery( { composeId }, @@ -100,7 +102,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => { - + { + await deleteDomain({ + domainId: item.domainId, + }) + .then((data) => { + refetch(); + toast.success("Domain deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting domain"); + }); + }} + > + +
); diff --git a/apps/dokploy/components/dashboard/compose/enviroment/show.tsx b/apps/dokploy/components/dashboard/compose/enviroment/show.tsx deleted file mode 100644 index 4e9b2dfd5..000000000 --- a/apps/dokploy/components/dashboard/compose/enviroment/show.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { CodeEditor } from "@/components/shared/code-editor"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; -import { Toggle } from "@/components/ui/toggle"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { EyeIcon, EyeOffIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addEnvironmentSchema = z.object({ - environment: z.string(), -}); - -type EnvironmentSchema = z.infer; - -interface Props { - composeId: string; -} - -export const ShowEnvironmentCompose = ({ composeId }: Props) => { - const [isEnvVisible, setIsEnvVisible] = useState(true); - const { mutateAsync, isLoading } = api.compose.update.useMutation(); - - const { data, refetch } = api.compose.one.useQuery( - { - composeId, - }, - { - enabled: !!composeId, - }, - ); - const form = useForm({ - defaultValues: { - environment: "", - }, - resolver: zodResolver(addEnvironmentSchema), - }); - - useEffect(() => { - if (data) { - form.reset({ - environment: data.env || "", - }); - } - }, [form.reset, data, form]); - - const onSubmit = async (data: EnvironmentSchema) => { - mutateAsync({ - env: data.environment, - composeId, - }) - .then(async () => { - toast.success("Environments Added"); - await refetch(); - }) - .catch(() => { - toast.error("Error to add environment"); - }); - }; - - useEffect(() => { - if (isEnvVisible) { - if (data?.env) { - const maskedLines = data.env - .split("\n") - .map((line) => "*".repeat(line.length)) - .join("\n"); - form.reset({ - environment: maskedLines, - }); - } else { - form.reset({ - environment: "", - }); - } - } else { - form.reset({ - environment: data?.env || "", - }); - } - }, [form.reset, data, form, isEnvVisible]); - - return ( -
- - -
- Environment Settings - - You can add environment variables to your resource. - -
- - - {isEnvVisible ? ( - - ) : ( - - )} - -
- -
- - ( - - - - - - - - )} - /> - -
- -
- - -
-
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx index 439669669..a40cc3453 100644 --- a/apps/dokploy/components/dashboard/compose/general/actions.tsx +++ b/apps/dokploy/components/dashboard/compose/general/actions.tsx @@ -1,28 +1,17 @@ +import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; -import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react"; -import Link from "next/link"; +import { Ban, CheckCircle2, Hammer, Terminal } from "lucide-react"; +import { useRouter } from "next/router"; import { toast } from "sonner"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; -import { StartCompose } from "../start-compose"; -import { DeployCompose } from "./deploy-compose"; -import { RedbuildCompose } from "./rebuild-compose"; -import { StopCompose } from "./stop-compose"; interface Props { composeId: string; } export const ComposeActions = ({ composeId }: Props) => { + const router = useRouter(); const { data, refetch } = api.compose.one.useQuery( { composeId, @@ -30,33 +19,109 @@ export const ComposeActions = ({ composeId }: Props) => { { enabled: !!composeId }, ); const { mutateAsync: update } = api.compose.update.useMutation(); - - const extractDomains = (env: string) => { - const lines = env.split("\n"); - const hostLines = lines.filter((line) => { - const [key, value] = line.split("="); - return key?.trim().endsWith("_HOST"); - }); - - const hosts = hostLines.map((line) => { - const [key, value] = line.split("="); - return value ? value.trim() : ""; - }); - - return hosts; - }; - - const domains = extractDomains(data?.env || ""); - + const { mutateAsync: deploy } = api.compose.deploy.useMutation(); + const { mutateAsync: redeploy } = api.compose.redeploy.useMutation(); + const { mutateAsync: start, isLoading: isStarting } = + api.compose.start.useMutation(); + const { mutateAsync: stop, isLoading: isStopping } = + api.compose.stop.useMutation(); return (
- - + { + await deploy({ + composeId: composeId, + }) + .then(() => { + toast.success("Compose deployed successfully"); + refetch(); + router.push( + `/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`, + ); + }) + .catch(() => { + toast.error("Error deploying compose"); + }); + }} + > + + + { + await redeploy({ + composeId: composeId, + }) + .then(() => { + toast.success("Compose rebuilt successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error rebuilding compose"); + }); + }} + > + + {data?.composeType === "docker-compose" && data?.composeStatus === "idle" ? ( - + { + await start({ + composeId: composeId, + }) + .then(() => { + toast.success("Compose started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting compose"); + }); + }} + > + + ) : ( - + { + await stop({ + composeId: composeId, + }) + .then(() => { + toast.success("Compose stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping compose"); + }); + }} + > + + )} { await refetch(); }) .catch(() => { - toast.error("Error to update Auto Deploy"); + toast.error("Error updating Auto Deploy"); }); }} className="flex flex-row gap-2 items-center" />
- {domains.length > 0 && ( - - - - - - Domains detected - - - {domains.map((host, index) => { - const url = - host.startsWith("http://") || host.startsWith("https://") - ? host - : `http://${host}`; - - return ( - - - {host} - - - - ); - })} - - - - )}
); }; diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx index 90bf6e301..b368aafce 100644 --- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx +++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx @@ -77,7 +77,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => { }); }) .catch((e) => { - toast.error("Error to update the compose config"); + toast.error("Error updating the Compose config"); }); }; return ( diff --git a/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx b/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx deleted file mode 100644 index e9d5dfc19..000000000 --- a/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { toast } from "sonner"; - -interface Props { - composeId: string; -} - -export const DeployCompose = ({ composeId }: Props) => { - const { data, refetch } = api.compose.one.useQuery( - { - composeId, - }, - { enabled: !!composeId }, - ); - - const { mutateAsync: deploy } = api.compose.deploy.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will deploy the compose - - - - Cancel - { - toast.success("Deploying Compose...."); - - await refetch(); - await deploy({ - composeId, - }).catch(() => { - toast.error("Error to deploy Compose"); - }); - - await refetch(); - }} - > - Confirm - - - - - ); -}; 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 cf5a7d5b0..1c06fe881 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 @@ -139,7 +139,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the Bitbucket provider"); + toast.error("Error saving the Bitbucket provider"); }); }; @@ -237,7 +237,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -247,7 +247,12 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.username} + + { await refetch(); }) .catch(() => { - toast.error("Error to save the Git provider"); + toast.error("Error saving the Git provider"); }); }; 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 bd5e8874d..7787cb3ce 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 @@ -133,7 +133,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the github provider"); + toast.error("Error saving the Github provider"); }); }; @@ -228,7 +228,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -238,7 +238,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.login} + + { await refetch(); }) .catch(() => { - toast.error("Error to save the gitlab provider"); + toast.error("Error saving the Gitlab provider"); }); }; @@ -250,7 +250,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => { {repositories?.map((repo) => { return ( { form.setValue("repository", { @@ -262,7 +262,12 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => { form.setValue("branch", ""); }} > - {repo.name} + + {repo.name} + + {repo.owner.username} + + { toast.success("Compose updated"); }) .catch(() => { - toast.error("Error to randomize the compose"); + toast.error("Error randomizing the compose"); }); }; @@ -105,7 +105,7 @@ export const RandomizeCompose = ({ composeId }: Props) => { toast.success("Compose randomized"); }) .catch(() => { - toast.error("Error to randomize the compose"); + toast.error("Error randomizing the compose"); }); }; diff --git a/apps/dokploy/components/dashboard/compose/general/rebuild-compose.tsx b/apps/dokploy/components/dashboard/compose/general/rebuild-compose.tsx deleted file mode 100644 index 199d4f93d..000000000 --- a/apps/dokploy/components/dashboard/compose/general/rebuild-compose.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Hammer } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - composeId: string; -} - -export const RedbuildCompose = ({ composeId }: Props) => { - const { data } = api.compose.one.useQuery( - { - composeId, - }, - { enabled: !!composeId }, - ); - const { mutateAsync } = api.compose.redeploy.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you sure to rebuild the compose? - - - Is required to deploy at least 1 time in order to reuse the same - code - - - - Cancel - { - toast.success("Redeploying Compose...."); - await mutateAsync({ - composeId, - }) - .then(async () => { - await utils.compose.one.invalidate({ - composeId, - }); - }) - .catch(() => { - toast.error("Error to rebuild the compose"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx index ec441e021..8a2186d9e 100644 --- a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx @@ -73,7 +73,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => { toast.success("Fetched source type"); }) .catch((err) => { - toast.error("Error to fetch source type", { + toast.error("Error fetching source type", { description: err.message, }); }); diff --git a/apps/dokploy/components/dashboard/compose/general/stop-compose.tsx b/apps/dokploy/components/dashboard/compose/general/stop-compose.tsx deleted file mode 100644 index 2bb3cdebe..000000000 --- a/apps/dokploy/components/dashboard/compose/general/stop-compose.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Ban } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - composeId: string; -} - -export const StopCompose = ({ composeId }: Props) => { - const { data } = api.compose.one.useQuery( - { - composeId, - }, - { enabled: !!composeId }, - ); - const { mutateAsync, isLoading } = api.compose.stop.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - Are you sure to stop the compose? - - This will stop the compose services - - - - Cancel - { - await mutateAsync({ - composeId, - }) - .then(async () => { - await utils.compose.one.invalidate({ - composeId, - }); - toast.success("Compose stopped succesfully"); - }) - .catch(() => { - toast.error("Error to stop the compose"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx new file mode 100644 index 000000000..d166f933f --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -0,0 +1,165 @@ +import { badgeStateColor } from "@/components/dashboard/application/logs/show"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; +export const DockerLogs = dynamic( + () => + import("@/components/dashboard/docker/logs/docker-logs-id").then( + (e) => e.DockerLogsId, + ), + { + ssr: false, + }, +); + +interface Props { + appName: string; + serverId?: string; +} + +badgeStateColor; + +export const ShowDockerLogsStack = ({ appName, serverId }: Props) => { + const [option, setOption] = useState<"swarm" | "native">("native"); + const [containerId, setContainerId] = useState(); + + const { data: services, isLoading: servicesLoading } = + api.docker.getStackContainersByAppName.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "swarm", + }, + ); + + const { data: containers, isLoading: containersLoading } = + api.docker.getContainersByAppNameMatch.useQuery( + { + appName, + appType: "stack", + serverId, + }, + { + enabled: !!appName && option === "native", + }, + ); + + useEffect(() => { + if (option === "native") { + if (containers && containers?.length > 0) { + setContainerId(containers[0]?.containerId); + } + } else { + if (services && services?.length > 0) { + setContainerId(services[0]?.containerId); + } + } + }, [option, services, containers]); + + const isLoading = option === "native" ? containersLoading : servicesLoading; + const containersLenght = + option === "native" ? containers?.length : services?.length; + + return ( + + + Logs + + Watch the logs of the application in real time + + + + +
+ +
+ + {option === "native" ? "Native" : "Swarm"} + + { + setOption(checked ? "native" : "swarm"); + }} + /> +
+
+ + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index 992086945..4530e0ddd 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx @@ -1,3 +1,5 @@ +import { badgeStateColor } from "@/components/dashboard/application/logs/show"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, @@ -87,7 +89,10 @@ export const ShowDockerLogsCompose = ({ key={container.containerId} value={container.containerId} > - {container.name} ({container.containerId}) {container.state} + {container.name} ({container.containerId}){" "} + + {container.state} + ))} Containers ({data?.length}) @@ -96,8 +101,8 @@ export const ShowDockerLogsCompose = ({ diff --git a/apps/dokploy/components/dashboard/compose/start-compose.tsx b/apps/dokploy/components/dashboard/compose/start-compose.tsx deleted file mode 100644 index 20f990bb3..000000000 --- a/apps/dokploy/components/dashboard/compose/start-compose.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { CheckCircle2 } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - composeId: string; -} - -export const StartCompose = ({ composeId }: Props) => { - const { mutateAsync, isLoading } = api.compose.start.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you sure to start the compose? - - - This will start the compose - - - - Cancel - { - await mutateAsync({ - composeId, - }) - .then(async () => { - await utils.compose.one.invalidate({ - composeId, - }); - toast.success("Compose started succesfully"); - }) - .catch(() => { - toast.error("Error to start the Compose"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/compose/stop-compose.tsx b/apps/dokploy/components/dashboard/compose/stop-compose.tsx deleted file mode 100644 index 3080e755a..000000000 --- a/apps/dokploy/components/dashboard/compose/stop-compose.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Ban } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - composeId: string; -} - -export const StopCompose = ({ composeId }: Props) => { - const { mutateAsync, isLoading } = api.compose.stop.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you absolutely sure to stop the compose? - - - This will stop the compose - - - - Cancel - { - await mutateAsync({ - composeId, - }) - .then(async () => { - await utils.compose.one.invalidate({ - composeId, - }); - toast.success("Compose stopped succesfully"); - }) - .catch(() => { - toast.error("Error to stop the Compose"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/compose/update-compose.tsx b/apps/dokploy/components/dashboard/compose/update-compose.tsx index 5991c03da..3120f2d4c 100644 --- a/apps/dokploy/components/dashboard/compose/update-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/update-compose.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { SquarePen } from "lucide-react"; +import { PenBoxIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -76,14 +76,14 @@ export const UpdateCompose = ({ composeId }: Props) => { description: formData.description || "", }) .then(() => { - toast.success("Compose updated succesfully"); + toast.success("Compose updated successfully"); utils.compose.one.invalidate({ composeId: composeId, }); setIsOpen(false); }) .catch(() => { - toast.error("Error to update the Compose"); + toast.error("Error updating the Compose"); }) .finally(() => {}); }; @@ -91,8 +91,12 @@ export const UpdateCompose = ({ composeId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx b/apps/dokploy/components/dashboard/database/backups/add-backup.tsx index 94f072e74..5f349b242 100644 --- a/apps/dokploy/components/dashboard/database/backups/add-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/add-backup.tsx @@ -125,7 +125,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => { refetch(); }) .catch(() => { - toast.error("Error to create a backup"); + toast.error("Error creating a backup"); }); }; return ( diff --git a/apps/dokploy/components/dashboard/database/backups/delete-backup.tsx b/apps/dokploy/components/dashboard/database/backups/delete-backup.tsx deleted file mode 100644 index 1a570b549..000000000 --- a/apps/dokploy/components/dashboard/database/backups/delete-backup.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { TrashIcon } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - backupId: string; - refetch: () => void; -} - -export const DeleteBackup = ({ backupId, refetch }: Props) => { - const { mutateAsync, isLoading } = api.backup.remove.useMutation(); - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - backup - - - - Cancel - { - await mutateAsync({ - backupId, - }) - .then(() => { - refetch(); - - toast.success("Backup delete succesfully"); - }) - .catch(() => { - toast.error("Error to delete the backup"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/postgres/backups/show-backup-postgres.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx similarity index 63% rename from apps/dokploy/components/dashboard/postgres/backups/show-backup-postgres.tsx rename to apps/dokploy/components/dashboard/database/backups/show-backups.tsx index c5a36a6ab..21fe28d4a 100644 --- a/apps/dokploy/components/dashboard/postgres/backups/show-backup-postgres.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -1,3 +1,4 @@ +import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; import { Card, @@ -13,31 +14,47 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; -import { DatabaseBackup, Play } from "lucide-react"; +import { DatabaseBackup, Play, Trash2 } from "lucide-react"; import Link from "next/link"; import React from "react"; import { toast } from "sonner"; -import { AddBackup } from "../../database/backups/add-backup"; -import { DeleteBackup } from "../../database/backups/delete-backup"; -import { UpdateBackup } from "../../database/backups/update-backup"; +import type { ServiceType } from "../../application/advanced/show-resources"; +import { AddBackup } from "./add-backup"; +import { UpdateBackup } from "./update-backup"; + interface Props { - postgresId: string; + id: string; + type: Exclude; } - -export const ShowBackupPostgres = ({ postgresId }: Props) => { +export const ShowBackups = ({ id, type }: Props) => { + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; const { data } = api.destination.all.useQuery(); - const { data: postgres, refetch: refetchPostgres } = - api.postgres.one.useQuery( - { - postgresId, - }, - { - enabled: !!postgresId, - }, - ); + const { data: postgres, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); - const { mutateAsync: manualBackup, isLoading: isManualBackup } = - api.backup.manualBackupPostgres.useMutation(); + const mutationMap = { + postgres: () => api.backup.manualBackupPostgres.useMutation(), + mysql: () => api.backup.manualBackupMySql.useMutation(), + mariadb: () => api.backup.manualBackupMariadb.useMutation(), + mongo: () => api.backup.manualBackupMongo.useMutation(), + }; + + const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[ + type + ] + ? mutationMap[type]() + : api.backup.manualBackupMongo.useMutation(); + + const { mutateAsync: deleteBackup, isLoading: isRemoving } = + api.backup.remove.useMutation(); return ( @@ -51,25 +68,21 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
{postgres && postgres?.backups?.length > 0 && ( - + )} {data?.length === 0 ? (
- + To create a backup it is required to set at least 1 provider. Please, go to{" "} - Settings + S3 Destinations {" "} to do so. @@ -83,9 +96,9 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => { No backups configured
) : ( @@ -145,7 +158,7 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => { }) .catch(() => { toast.error( - "Error to Create the manual backup", + "Error creating the manual backup", ); }); }} @@ -158,12 +171,34 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => { - + { + await deleteBackup({ + backupId: backup.backupId, + }) + .then(() => { + refetch(); + toast.success("Backup deleted successfully"); + }) + .catch(() => { + toast.error("Error deleting backup"); + }); + }} + > + +
diff --git a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx index 4c60a31b2..0083bb1df 100644 --- a/apps/dokploy/components/dashboard/database/backups/update-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/update-backup.tsx @@ -109,15 +109,19 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => { setIsOpen(false); }) .catch(() => { - toast.error("Error to update the backup"); + toast.error("Error updating the Backup"); }); }; return ( - diff --git a/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx b/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx index 25d78dd7d..1f1591c9d 100644 --- a/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx +++ b/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx @@ -1,3 +1,4 @@ +import { CodeEditor } from "@/components/shared/code-editor"; import { Dialog, DialogContent, @@ -34,7 +35,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => { View Config - + Container Config @@ -44,7 +45,13 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
-							{JSON.stringify(data, null, 2)}
+							
 						
diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 6fc0ab48a..b638991cf 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -1,114 +1,296 @@ +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Terminal } from "@xterm/xterm"; +import { api } from "@/utils/api"; +import { Download as DownloadIcon, Loader2 } from "lucide-react"; import React, { useEffect, useRef } from "react"; -import { FitAddon } from "xterm-addon-fit"; -import "@xterm/xterm/css/xterm.css"; +import { LineCountFilter } from "./line-count-filter"; +import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter"; +import { StatusLogsFilter } from "./status-logs-filter"; +import { TerminalLine } from "./terminal-line"; +import { type LogLine, getLogType, parseLogs } from "./utils"; interface Props { - id: string; containerId: string; serverId?: string | null; + runType: "swarm" | "native"; } +export const priorities = [ + { + label: "Info", + value: "info", + }, + { + label: "Success", + value: "success", + }, + { + label: "Warning", + value: "warning", + }, + { + label: "Debug", + value: "debug", + }, + { + label: "Error", + value: "error", + }, +]; + export const DockerLogsId: React.FC = ({ - id, containerId, serverId, + runType, }) => { - const [term, setTerm] = React.useState(); - const [lines, setLines] = React.useState(40); - const wsRef = useRef(null); // Ref to hold WebSocket instance + const { data } = api.docker.getConfig.useQuery( + { + containerId, + serverId: serverId ?? undefined, + }, + { + enabled: !!containerId, + }, + ); + + const [rawLogs, setRawLogs] = React.useState(""); + const [filteredLogs, setFilteredLogs] = React.useState([]); + const [autoScroll, setAutoScroll] = React.useState(true); + const [lines, setLines] = React.useState(100); + const [search, setSearch] = React.useState(""); + const [showTimestamp, setShowTimestamp] = React.useState(true); + const [since, setSince] = React.useState("all"); + const [typeFilter, setTypeFilter] = React.useState([]); + const scrollRef = useRef(null); + const [isLoading, setIsLoading] = React.useState(false); + + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; + + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + setAutoScroll(isAtBottom); + }; + + const handleSearch = (e: React.ChangeEvent) => { + setSearch(e.target.value || ""); + }; + + const handleLines = (lines: number) => { + setRawLogs(""); + setFilteredLogs([]); + setLines(lines); + }; + + const handleSince = (value: TimeFilter) => { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); + }; useEffect(() => { - // if (containerId === "select-a-container") { - // return; - // } - const container = document.getElementById(id); - if (container) { - container.innerHTML = ""; - } + if (!containerId) return; - if (wsRef.current) { - if (wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.close(); - } - wsRef.current = null; - } - const termi = new Terminal({ - cursorBlink: true, - cols: 80, - rows: 30, - lineHeight: 1.25, - fontWeight: 400, - fontSize: 14, - fontFamily: - 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', - - convertEol: true, - theme: { - cursor: "transparent", - background: "rgba(0, 0, 0, 0)", - }, - }); + let isCurrentConnection = true; + let noDataTimeout: NodeJS.Timeout; + setIsLoading(true); + setRawLogs(""); + setFilteredLogs([]); const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const params = new globalThis.URLSearchParams({ + containerId, + tail: lines.toString(), + since, + search, + runType, + }); - const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`; + if (serverId) { + params.append("serverId", serverId); + } + + const wsUrl = `${protocol}//${ + window.location.host + }/docker-container-logs?${params.toString()}`; + console.log("Connecting to WebSocket:", wsUrl); const ws = new WebSocket(wsUrl); - wsRef.current = ws; - const fitAddon = new FitAddon(); - termi.loadAddon(fitAddon); - // @ts-ignore - termi.open(container); - fitAddon.fit(); - termi.focus(); - setTerm(termi); - ws.onerror = (error) => { - console.error("WebSocket error: ", error); + const resetNoDataTimeout = () => { + if (noDataTimeout) clearTimeout(noDataTimeout); + noDataTimeout = setTimeout(() => { + if (isCurrentConnection) { + setIsLoading(false); + } + }, 2000); // Wait 2 seconds for data before showing "No logs found" + }; + + ws.onopen = () => { + if (!isCurrentConnection) { + ws.close(); + return; + } + console.log("WebSocket connected"); + resetNoDataTimeout(); }; ws.onmessage = (e) => { - termi.write(e.data); + if (!isCurrentConnection) return; + setRawLogs((prev) => prev + e.data); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); + }; + + ws.onerror = (error) => { + if (!isCurrentConnection) return; + console.error("WebSocket error:", error); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); }; ws.onclose = (e) => { - console.log(e.reason); - - termi.write(`Connection closed!\nReason: ${e.reason}\n`); - wsRef.current = null; + if (!isCurrentConnection) return; + console.log("WebSocket closed:", e.reason); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); }; + return () => { - if (wsRef.current?.readyState === WebSocket.OPEN) { + isCurrentConnection = false; + if (noDataTimeout) clearTimeout(noDataTimeout); + if (ws.readyState === WebSocket.OPEN) { ws.close(); - wsRef.current = null; } }; - }, [lines, containerId]); + }, [containerId, serverId, lines, search, since]); + + const handleDownload = () => { + const logContent = filteredLogs + .map( + ({ timestamp, message }: { timestamp: Date | null; message: string }) => + `${timestamp?.toISOString() || "No timestamp"} ${message}`, + ) + .join("\n"); + + const blob = new Blob([logContent], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + const appName = data.Name.replace("/", "") || "app"; + const isoDate = new Date().toISOString(); + a.href = url; + a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate + .slice(11, 19) + .replace(/:/g, "")}.log.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const handleFilter = (logs: LogLine[]) => { + return logs.filter((log) => { + const logType = getLogType(log.message).type; + + if (typeFilter.length === 0) { + return true; + } + + return typeFilter.includes(logType); + }); + }; useEffect(() => { - term?.clear(); - }, [lines, term]); + setRawLogs(""); + setFilteredLogs([]); + }, [containerId]); + + useEffect(() => { + const logs = parseLogs(rawLogs); + const filtered = handleFilter(logs); + setFilteredLogs(filtered); + }, [rawLogs, search, lines, since, typeFilter]); + + useEffect(() => { + scrollToBottom(); + + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [filteredLogs, autoScroll]); return (
-
- - { - setLines(Number(e.target.value) || 1); - }} - /> -
+
+
+
+
+ -
-
+ + + + + +
+ + +
+
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) + ) : isLoading ? ( +
+ +
+ ) : ( +
+ No logs found +
+ )} +
+
); diff --git a/apps/dokploy/components/dashboard/docker/logs/line-count-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/line-count-filter.tsx new file mode 100644 index 000000000..dd7b63af5 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/line-count-filter.tsx @@ -0,0 +1,173 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { Command as CommandPrimitive } from "cmdk"; +import { debounce } from "lodash"; +import { CheckIcon, Hash } from "lucide-react"; +import React, { useCallback, useRef } from "react"; + +const lineCountOptions = [ + { label: "100 lines", value: 100 }, + { label: "300 lines", value: 300 }, + { label: "500 lines", value: 500 }, + { label: "1000 lines", value: 1000 }, + { label: "5000 lines", value: 5000 }, +] as const; + +interface LineCountFilterProps { + value: number; + onValueChange: (value: number) => void; + title?: string; +} + +export function LineCountFilter({ + value, + onValueChange, + title = "Limit to", +}: LineCountFilterProps) { + const [open, setOpen] = React.useState(false); + const [inputValue, setInputValue] = React.useState(""); + const pendingValueRef = useRef(null); + + const isPresetValue = lineCountOptions.some( + (option) => option.value === value, + ); + + const debouncedValueChange = useCallback( + debounce((numValue: number) => { + if (numValue > 0 && numValue !== value) { + onValueChange(numValue); + pendingValueRef.current = null; + } + }, 500), + [onValueChange, value], + ); + + const handleInputChange = (input: string) => { + setInputValue(input); + + // Extract numbers from input and convert + const numValue = Number.parseInt(input.replace(/[^0-9]/g, "")); + if (!Number.isNaN(numValue)) { + pendingValueRef.current = numValue; + debouncedValueChange(numValue); + } + }; + + const handleSelect = (selectedValue: string) => { + const preset = lineCountOptions.find((opt) => opt.label === selectedValue); + if (preset) { + if (preset.value !== value) { + onValueChange(preset.value); + } + setInputValue(""); + setOpen(false); + return; + } + + const numValue = Number.parseInt(selectedValue); + if ( + !Number.isNaN(numValue) && + numValue > 0 && + numValue !== value && + numValue !== pendingValueRef.current + ) { + onValueChange(numValue); + setInputValue(""); + setOpen(false); + } + }; + + React.useEffect(() => { + return () => { + debouncedValueChange.cancel(); + }; + }, [debouncedValueChange]); + + const displayValue = isPresetValue + ? lineCountOptions.find((option) => option.value === value)?.label + : `${value} lines`; + + return ( + + + + + + +
+ + { + if (e.key === "Enter") { + e.preventDefault(); + const numValue = Number.parseInt( + inputValue.replace(/[^0-9]/g, ""), + ); + if ( + !Number.isNaN(numValue) && + numValue > 0 && + numValue !== value && + numValue !== pendingValueRef.current + ) { + handleSelect(inputValue); + } + } + }} + /> +
+ + + {lineCountOptions.map((option) => { + const isSelected = value === option.value; + return ( + handleSelect(option.label)} + className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground" + > +
+ +
+ {option.label} +
+ ); + })} +
+
+
+
+
+ ); +} + +export default LineCountFilter; diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx index c3d38d986..619b25d0c 100644 --- a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx @@ -47,9 +47,9 @@ export const ShowDockerModalLogs = ({
diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx new file mode 100644 index 000000000..36719bb07 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx @@ -0,0 +1,58 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import dynamic from "next/dynamic"; +import type React from "react"; +export const DockerLogsId = dynamic( + () => + import("@/components/dashboard/docker/logs/docker-logs-id").then( + (e) => e.DockerLogsId, + ), + { + ssr: false, + }, +); + +interface Props { + containerId: string; + children?: React.ReactNode; + serverId?: string | null; +} + +export const ShowDockerModalStackLogs = ({ + containerId, + children, + serverId, +}: Props) => { + return ( + + + e.preventDefault()} + > + {children} + + + + + View Logs + View the logs for {containerId} + +
+ +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx new file mode 100644 index 000000000..b7caafe71 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx @@ -0,0 +1,125 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandGroup, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { CheckIcon } from "lucide-react"; +import React from "react"; + +export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; + +const timeRanges: Array<{ label: string; value: TimeFilter }> = [ + { + label: "All time", + value: "all", + }, + { + label: "Last hour", + value: "1h", + }, + { + label: "Last 6 hours", + value: "6h", + }, + { + label: "Last 24 hours", + value: "24h", + }, + { + label: "Last 7 days", + value: "168h", + }, + { + label: "Last 30 days", + value: "720h", + }, +] as const; + +interface SinceLogsFilterProps { + value: TimeFilter; + onValueChange: (value: TimeFilter) => void; + showTimestamp: boolean; + onTimestampChange: (show: boolean) => void; + title?: string; +} + +export function SinceLogsFilter({ + value, + onValueChange, + showTimestamp, + onTimestampChange, + title = "Time range", +}: SinceLogsFilterProps) { + const selectedLabel = + timeRanges.find((range) => range.value === value)?.label ?? + "Select time range"; + + return ( + + + + + + + + + {timeRanges.map((range) => { + const isSelected = value === range.value; + return ( + { + if (!isSelected) { + onValueChange(range.value); + } + }} + > +
+ +
+ {range.label} +
+ ); + })} +
+
+
+ +
+ Show timestamps + +
+
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx new file mode 100644 index 000000000..3ef11517a --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx @@ -0,0 +1,170 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandGroup, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { CheckIcon } from "lucide-react"; +import type React from "react"; + +interface StatusLogsFilterProps { + value?: string[]; + setValue?: (value: string[]) => void; + title?: string; + options: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + }[]; +} + +export function StatusLogsFilter({ + value = [], + setValue, + title, + options, +}: StatusLogsFilterProps) { + const selectedValues = new Set(value as string[]); + const allSelected = selectedValues.size === 0; + + const getSelectedBadges = () => { + if (allSelected) { + return ( + + All + + ); + } + + if (selectedValues.size >= 1) { + const selected = options.find((opt) => selectedValues.has(opt.value)); + return ( + <> + + {selected?.label} + + {selectedValues.size > 1 && ( + + +{selectedValues.size - 1} + + )} + + ); + } + + return null; + }; + + return ( + + + + + + + + + { + setValue?.([]); // Empty array means "All" + }} + > +
+ +
+ All +
+ {options.map((option) => { + const isSelected = selectedValues.has(option.value); + return ( + { + const newValues = new Set(selectedValues); + if (isSelected) { + newValues.delete(option.value); + } else { + newValues.add(option.value); + } + setValue?.(Array.from(newValues)); + }} + > +
+ +
+ {option.icon && ( + + )} + + {option.label} + +
+ ); + })} +
+
+
+
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx new file mode 100644 index 000000000..c25acc67f --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -0,0 +1,139 @@ +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { FancyAnsi } from "fancy-ansi"; +import { escapeRegExp } from "lodash"; +import React from "react"; +import { type LogLine, getLogType } from "./utils"; + +interface LogLineProps { + log: LogLine; + noTimestamp?: boolean; + searchTerm?: string; +} + +const fancyAnsi = new FancyAnsi(); + +export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { + const { timestamp, message, rawTimestamp } = log; + const { type, variant, color } = getLogType(message); + + const formattedTime = timestamp + ? timestamp.toLocaleString([], { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + year: "2-digit", + second: "2-digit", + }) + : "--- No time found ---"; + + const highlightMessage = (text: string, term: string) => { + if (!term) { + return ( + + ); + } + + const htmlContent = fancyAnsi.toHtml(text); + const modifiedContent = htmlContent.replace( + /]*)>([^<]*)<\/span>/g, + (match, attrs, content) => { + const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi"); + if (!content.match(searchRegex)) return match; + + const segments = content.split(searchRegex); + const wrappedSegments = segments + .map((segment: string) => + segment.toLowerCase() === term.toLowerCase() + ? `${segment}` + : segment, + ) + .join(""); + + return `${wrappedSegments}`; + }, + ); + + return ( + + ); + }; + + const tooltip = (color: string, timestamp: string | null) => { + const square = ( +
+ ); + return timestamp ? ( + + + {square} + + +

+

{timestamp}
+

+
+
+
+
+ ) : ( + square + ); + }; + + return ( +
+ {" "} +
+ {/* Icon to expand the log item maybe implement a colapsible later */} + {/* */} + {tooltip(color, rawTimestamp)} + {!noTimestamp && ( + + {formattedTime} + + )} + + + {type} + +
+ + {highlightMessage(message, searchTerm || "")} + +
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts new file mode 100644 index 000000000..698311a79 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -0,0 +1,152 @@ +export type LogType = "error" | "warning" | "success" | "info" | "debug"; +export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; + +export interface LogLine { + rawTimestamp: string | null; + timestamp: Date | null; + message: string; +} + +interface LogStyle { + type: LogType; + variant: LogVariant; + color: string; +} + +const LOG_STYLES: Record = { + error: { + type: "error", + variant: "red", + color: "bg-red-500/40", + }, + warning: { + type: "warning", + variant: "orange", + color: "bg-orange-500/40", + }, + debug: { + type: "debug", + variant: "yellow", + color: "bg-yellow-500/40", + }, + success: { + type: "success", + variant: "green", + color: "bg-green-500/40", + }, + info: { + type: "info", + variant: "blue", + color: "bg-blue-600/40", + }, +} as const; + +export function parseLogs(logString: string): LogLine[] { + // Regex to match the log line format + // Example of return : + // 1 2024-12-10T10:00:00.000Z The server is running on port 8080 + // Should return : + // { timestamp: new Date("2024-12-10T10:00:00.000Z"), + // message: "The server is running on port 8080" } + const logRegex = + /^(?:(\d+)\s+)?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC)?\s*(.*)$/; + + return logString + .split("\n") + .map((line) => line.trim()) + .filter((line) => line !== "") + .map((line) => { + const match = line.match(logRegex); + if (!match) return null; + + const [, , timestamp, message] = match; + + if (!message?.trim()) return null; + + // Delete other timestamps and keep only the one from --timestamps + const cleanedMessage = message + ?.replace( + /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g, + "", + ) + .trim(); + + return { + rawTimestamp: timestamp ?? null, + timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null, + message: cleanedMessage, + }; + }) + .filter((log) => log !== null); +} + +// Detect log type based on message content +export const getLogType = (message: string): LogStyle => { + const lowerMessage = message.toLowerCase(); + + if ( + /(?:^|\s)(?:info|inf|information):?\s/i.test(lowerMessage) || + /\[(?:info|information)\]/i.test(lowerMessage) || + /\b(?:status|state|current|progress)\b:?\s/i.test(lowerMessage) || + /\b(?:processing|executing|performing)\b/i.test(lowerMessage) + ) { + return LOG_STYLES.info; + } + + if ( + /(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) || + /\b(?:exception|failed|failure)\b/i.test(lowerMessage) || + /(?:stack\s?trace):\s*$/i.test(lowerMessage) || + /^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) || + /\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) || + /Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) || + /\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) || + /\[(?:error|err|fatal)\]/i.test(lowerMessage) || + /\b(?:crash|critical|fatal)\b/i.test(lowerMessage) || + /\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage) + ) { + return LOG_STYLES.error; + } + + if ( + /(?:^|\s)(?:warning|warn):?\s/i.test(lowerMessage) || + /\[(?:warn(?:ing)?|attention)\]/i.test(lowerMessage) || + /(?:deprecated|obsolete)\s+(?:since|in|as\s+of)/i.test(lowerMessage) || + /\b(?:caution|attention|notice):\s/i.test(lowerMessage) || + /(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) || + /(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) || + /\b(?:deprecated|obsolete)\b/i.test(lowerMessage) || + /\b(?:unstable|experimental)\b/i.test(lowerMessage) + ) { + return LOG_STYLES.warning; + } + + if ( + /(?:successfully|complete[d]?)\s+(?:initialized|started|completed|created|done|deployed)/i.test( + lowerMessage, + ) || + /\[(?:success|ok|done)\]/i.test(lowerMessage) || + /(?:listening|running)\s+(?:on|at)\s+(?:port\s+)?\d+/i.test(lowerMessage) || + /(?:connected|established|ready)\s+(?:to|for|on)/i.test(lowerMessage) || + /\b(?:loaded|mounted|initialized)\s+successfully\b/i.test(lowerMessage) || + /✓|√|✅|\[ok\]|done!/i.test(lowerMessage) || + /\b(?:success(?:ful)?|completed|ready)\b/i.test(lowerMessage) || + /\b(?:started|starting|active)\b/i.test(lowerMessage) + ) { + return LOG_STYLES.success; + } + + if ( + /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) || + /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test( + lowerMessage, + ) || + /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test( + lowerMessage, + ) + ) { + return LOG_STYLES.debug; + } + + return LOG_STYLES.info; +}; diff --git a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx index e55e6271f..c66c9b9ba 100644 --- a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx +++ b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx @@ -9,10 +9,18 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table"; -import { ChevronDown } from "lucide-react"; +import { ChevronDown, Container } from "lucide-react"; import * as React from "react"; +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -71,139 +79,164 @@ export const ShowContainers = ({ serverId }: Props) => { }); return ( -
-
-
- - table.getColumn("name")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
-
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), +
+ +
+ + + + Docker Containers + + + See all the containers of your dokploy server + + + +
+
+
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. )} - - ); - })} -
- ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
+ + + )} +
+
+ )} +
+ {data && data?.length > 0 && ( +
+
+ + +
+
)} - - - )} -
- {data && data?.length > 0 && ( -
-
- - +
-
- )} -
+ +
+
); }; diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx index c3dba4f51..90aa2b406 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx @@ -59,7 +59,10 @@ export const DockerTerminalModal = ({ {children} - + event.preventDefault()} + > Docker Terminal @@ -73,7 +76,7 @@ export const DockerTerminalModal = ({ serverId={serverId || ""} /> - + event.preventDefault()}> Are you sure you want to close the terminal? diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 4008d6fd5..bf14680a4 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -4,6 +4,7 @@ import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AttachAddon } from "@xterm/addon-attach"; +import { useTheme } from "next-themes"; interface Props { id: string; @@ -18,6 +19,7 @@ export const DockerTerminal: React.FC = ({ }) => { const termRef = useRef(null); const [activeWay, setActiveWay] = React.useState("bash"); + const { resolvedTheme } = useTheme(); useEffect(() => { const container = document.getElementById(id); if (container) { @@ -25,13 +27,12 @@ export const DockerTerminal: React.FC = ({ } const term = new Terminal({ cursorBlink: true, - cols: 80, - rows: 30, lineHeight: 1.4, convertEol: true, theme: { - cursor: "transparent", + cursor: resolvedTheme === "light" ? "#000000" : "transparent", background: "rgba(0, 0, 0, 0)", + foreground: "currentColor", }, }); const addonFit = new FitAddon(); @@ -45,6 +46,7 @@ export const DockerTerminal: React.FC = ({ const addonAttach = new AttachAddon(ws); // @ts-ignore term.open(termRef.current); + // @ts-ignore term.loadAddon(addonFit); term.loadAddon(addonAttach); addonFit.fit(); @@ -66,7 +68,7 @@ export const DockerTerminal: React.FC = ({
-
+
diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx index 3dfe98754..fb5fe8f5c 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-file.tsx @@ -86,7 +86,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => { refetch(); }) .catch(() => { - toast.error("Error to update the traefik config"); + toast.error("Error updating the Traefik config"); }); }; diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx index 0aaf9990b..ed2ed1974 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx @@ -1,8 +1,15 @@ import { AlertBlock } from "@/components/shared/alert-block"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Tree } from "@/components/ui/file-tree"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { FileIcon, Folder, Loader2, Workflow } from "lucide-react"; +import { FileIcon, Folder, Link, Loader2, Workflow } from "lucide-react"; import React from "react"; import { ShowTraefikFile } from "./show-traefik-file"; @@ -27,53 +34,77 @@ export const ShowTraefikSystem = ({ serverId }: Props) => { ); return ( -
-
- {isError && ( - - {error?.message} - - )} - {isLoading && ( -
- - Loading... - - -
- )} - {directories?.length === 0 && ( -
- - No directories or files detected in {"'/etc/dokploy/traefik'"} - - -
- )} - {directories && directories?.length > 0 && ( - <> - setFile(item?.id || null)} - folderIcon={Folder} - itemIcon={Workflow} - /> -
- {file ? ( - - ) : ( -
- - No file selected - - -
- )} +
+ +
+ + + + Traefik File System + + + Manage all the files and directories in {"'/etc/dokploy/traefik'"} + . + + + + Adding invalid configuration to existing files, can break your + Traefik instance, preventing access to your applications. + + + +
+
+ {isError && ( + + {error?.message} + + )} + {isLoading && ( +
+ + Loading... + + +
+ )} + {directories?.length === 0 && ( +
+ + No directories or files detected in{" "} + {"'/etc/dokploy/traefik'"} + + +
+ )} + {directories && directories?.length > 0 && ( + <> + setFile(item?.id || null)} + folderIcon={Folder} + itemIcon={Workflow} + /> +
+ {file ? ( + + ) : ( +
+ + No file selected + + +
+ )} +
+ + )} +
- - )} -
+ +
+
); }; diff --git a/apps/dokploy/components/dashboard/mariadb/advanced/show-mariadb-advanced-settings.tsx b/apps/dokploy/components/dashboard/mariadb/advanced/show-mariadb-advanced-settings.tsx deleted file mode 100644 index 062fffcdc..000000000 --- a/apps/dokploy/components/dashboard/mariadb/advanced/show-mariadb-advanced-settings.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -import { ShowVolumes } from "../volumes/show-volumes"; -import { ShowMariadbResources } from "./show-mariadb-resources"; - -const addDockerImage = z.object({ - dockerImage: z.string().min(1, "Docker image is required"), - command: z.string(), -}); - -interface Props { - mariadbId: string; -} - -type AddDockerImage = z.infer; -export const ShowAdvancedMariadb = ({ mariadbId }: Props) => { - const { data, refetch } = api.mariadb.one.useQuery( - { - mariadbId, - }, - { enabled: !!mariadbId }, - ); - const { mutateAsync } = api.mariadb.update.useMutation(); - - const form = useForm({ - defaultValues: { - dockerImage: "", - command: "", - }, - resolver: zodResolver(addDockerImage), - }); - - useEffect(() => { - if (data) { - form.reset({ - dockerImage: data.dockerImage, - command: data.command || "", - }); - } - }, [data, form, form.formState.isSubmitSuccessful, form.reset]); - - const onSubmit = async (formData: AddDockerImage) => { - await mutateAsync({ - mariadbId, - dockerImage: formData?.dockerImage, - command: formData?.command, - }) - .then(async () => { - toast.success("Docker Image Updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error to Update the resources"); - }); - }; - return ( - <> -
- - - Advanced Settings - - -
- -
- ( - - Docker Image - - - - - - - )} - /> - - ( - - Command - - - - - - - )} - /> -
-
- -
-
- -
-
- - -
- - ); -}; diff --git a/apps/dokploy/components/dashboard/mariadb/advanced/show-mariadb-resources.tsx b/apps/dokploy/components/dashboard/mariadb/advanced/show-mariadb-resources.tsx deleted file mode 100644 index 60cdbda0c..000000000 --- a/apps/dokploy/components/dashboard/mariadb/advanced/show-mariadb-resources.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addResourcesMariadb = z.object({ - memoryReservation: z.number().nullable().optional(), - cpuLimit: z.number().nullable().optional(), - memoryLimit: z.number().nullable().optional(), - cpuReservation: z.number().nullable().optional(), -}); -interface Props { - mariadbId: string; -} - -type AddResourcesMariadb = z.infer; -export const ShowMariadbResources = ({ mariadbId }: Props) => { - const { data, refetch } = api.mariadb.one.useQuery( - { - mariadbId, - }, - { enabled: !!mariadbId }, - ); - const { mutateAsync, isLoading } = api.mariadb.update.useMutation(); - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(addResourcesMariadb), - }); - - useEffect(() => { - if (data) { - form.reset({ - cpuLimit: data?.cpuLimit || undefined, - cpuReservation: data?.cpuReservation || undefined, - memoryLimit: data?.memoryLimit || undefined, - memoryReservation: data?.memoryReservation || undefined, - }); - } - }, [data, form, form.formState.isSubmitSuccessful, form.reset]); - - const onSubmit = async (formData: AddResourcesMariadb) => { - await mutateAsync({ - mariadbId, - cpuLimit: formData.cpuLimit || null, - cpuReservation: formData.cpuReservation || null, - memoryLimit: formData.memoryLimit || null, - memoryReservation: formData.memoryReservation || null, - }) - .then(async () => { - toast.success("Resources Updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error to Update the resources"); - }); - }; - return ( - - - Resources - - If you want to decrease or increase the resources to a specific. - application or database - - - - - Please remember to click Redeploy after modify the resources to apply - the changes. - -
- -
- ( - - Memory Reservation - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - - )} - /> - - { - return ( - - Memory Limit - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - - { - return ( - - Cpu Limit - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - { - return ( - - Cpu Reservation - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> -
-
- -
-
- -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mariadb/backups/show-backup-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/backups/show-backup-mariadb.tsx deleted file mode 100644 index f3d214e3d..000000000 --- a/apps/dokploy/components/dashboard/mariadb/backups/show-backup-mariadb.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { api } from "@/utils/api"; -import { DatabaseBackup, Play } from "lucide-react"; -import Link from "next/link"; -import React from "react"; -import { toast } from "sonner"; -import { AddBackup } from "../../database/backups/add-backup"; -import { DeleteBackup } from "../../database/backups/delete-backup"; -import { UpdateBackup } from "../../database/backups/update-backup"; -interface Props { - mariadbId: string; -} - -export const ShowBackupMariadb = ({ mariadbId }: Props) => { - const { data } = api.destination.all.useQuery(); - const { data: mariadb, refetch: refetchMariadb } = api.mariadb.one.useQuery( - { - mariadbId, - }, - { - enabled: !!mariadbId, - }, - ); - - const { mutateAsync: manualBackup, isLoading: isManualBackup } = - api.backup.manualBackupMariadb.useMutation(); - - return ( - - -
- Backups - - Add backups to your database to save the data to a different - providers. - -
- - {mariadb && mariadb?.backups?.length > 0 && ( - - )} -
- - {data?.length === 0 ? ( -
- - - To create a backup it is required to set at least 1 provider. - Please, go to{" "} - - Settings - {" "} - to do so. - -
- ) : ( -
- {mariadb?.backups.length === 0 ? ( -
- - - No backups configured - - -
- ) : ( -
-
- {mariadb?.backups.map((backup) => ( -
-
-
-
- Destination - - {backup.destination.name} - -
-
- Database - - {backup.database} - -
-
- Scheduled - - {backup.schedule} - -
-
- Prefix Storage - - {backup.prefix} - -
-
- Enabled - - {backup.enabled ? "Yes" : "No"} - -
-
-
- - - - - - Run Manual Backup - - - - -
-
-
- ))} -
-
- )} -
- )} -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mariadb/delete-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/delete-mariadb.tsx deleted file mode 100644 index 26a6215f4..000000000 --- a/apps/dokploy/components/dashboard/mariadb/delete-mariadb.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const deleteMariadbSchema = z.object({ - projectName: z.string().min(1, { - message: "Database name is required", - }), -}); - -type DeleteMariadb = z.infer; - -interface Props { - mariadbId: string; -} -export const DeleteMariadb = ({ mariadbId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.mariadb.remove.useMutation(); - const { data } = api.mariadb.one.useQuery( - { mariadbId }, - { enabled: !!mariadbId }, - ); - const { push } = useRouter(); - const form = useForm({ - defaultValues: { - projectName: "", - }, - resolver: zodResolver(deleteMariadbSchema), - }); - - const onSubmit = async (formData: DeleteMariadb) => { - const expectedName = `${data?.name}/${data?.appName}`; - if (formData.projectName === expectedName) { - await mutateAsync({ mariadbId }) - .then((data) => { - push(`/dashboard/project/${data?.projectId}`); - toast.success("Database deleted successfully"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error deleting the database"); - }); - } else { - form.setError("projectName", { - message: "Database name does not match", - }); - } - }; - - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - database. If you are sure please enter the database name to delete - this database. - - -
-
- - ( - - - To confirm, type "{data?.name}/{data?.appName}" in the box - below - - - - - - - )} - /> - - -
- - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mariadb/general/deploy-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/deploy-mariadb.tsx deleted file mode 100644 index e3162f00f..000000000 --- a/apps/dokploy/components/dashboard/mariadb/general/deploy-mariadb.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { toast } from "sonner"; - -interface Props { - mariadbId: string; -} - -export const DeployMariadb = ({ mariadbId }: Props) => { - const { data, refetch } = api.mariadb.one.useQuery( - { - mariadbId, - }, - { enabled: !!mariadbId }, - ); - const { mutateAsync: deploy } = api.mariadb.deploy.useMutation(); - const { mutateAsync: changeStatus } = api.mariadb.changeStatus.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will deploy the mariadb database - - - - Cancel - { - await changeStatus({ - mariadbId, - applicationStatus: "running", - }) - .then(async () => { - toast.success("Deploying Database...."); - await refetch(); - await deploy({ - mariadbId, - }).catch(() => { - toast.error("Error to deploy Database"); - }); - await refetch(); - }) - .catch((e) => { - toast.error(e.message || "Error to deploy Database"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mariadb/general/reset-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/reset-mariadb.tsx deleted file mode 100644 index 3adb3b70d..000000000 --- a/apps/dokploy/components/dashboard/mariadb/general/reset-mariadb.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { RefreshCcw } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mariadbId: string; - appName: string; -} - -export const ResetMariadb = ({ mariadbId, appName }: Props) => { - const { refetch } = api.mariadb.one.useQuery( - { - mariadbId, - }, - { enabled: !!mariadbId }, - ); - const { mutateAsync: reload, isLoading } = api.mariadb.reload.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will reload the service - - - - Cancel - { - await reload({ - mariadbId, - appName, - }) - .then(() => { - toast.success("Service Reloaded"); - }) - .catch(() => { - toast.error("Error to reload the service"); - }); - await refetch(); - }} - > - Confirm - - - - - ); -}; 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 c06cacaa8..f20449178 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 @@ -72,7 +72,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the external port"); + toast.error("Error saving the external port"); }); }; diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx index 925e213d8..98773685f 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx @@ -1,26 +1,62 @@ +import { DialogAction } from "@/components/shared/dialog-action"; +import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Terminal } from "lucide-react"; -import React from "react"; +import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; +import React, { useState } from "react"; +import { toast } from "sonner"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; -import { StartMariadb } from "../start-mariadb"; -import { DeployMariadb } from "./deploy-mariadb"; -import { ResetMariadb } from "./reset-mariadb"; -import { StopMariadb } from "./stop-mariadb"; interface Props { mariadbId: string; } export const ShowGeneralMariadb = ({ mariadbId }: Props) => { - const { data } = api.mariadb.one.useQuery( + const { data, refetch } = api.mariadb.one.useQuery( { mariadbId, }, { enabled: !!mariadbId }, ); + const { mutateAsync: reload, isLoading: isReloading } = + api.mariadb.reload.useMutation(); + + const { mutateAsync: start, isLoading: isStarting } = + api.mariadb.start.useMutation(); + + const { mutateAsync: stop, isLoading: isStopping } = + api.mariadb.stop.useMutation(); + + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + api.mariadb.deployWithLogs.useSubscription( + { + mariadbId: mariadbId, + }, + { + enabled: isDeploying, + onData(log) { + if (!isDrawerOpen) { + setIsDrawerOpen(true); + } + + if (log === "Deployment completed successfully!") { + setIsDeploying(false); + } + const parsedLogs = parseLogs(log); + setFilteredLogs((prev) => [...prev, ...parsedLogs]); + }, + onError(error) { + console.error("Deployment logs error:", error); + setIsDeploying(false); + }, + }, + ); + return ( <>
@@ -29,12 +65,91 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => { Deploy Settings - - + { + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); + }} + > + + + { + await reload({ + mariadbId: mariadbId, + appName: data?.appName || "", + }) + .then(() => { + toast.success("Mariadb reloaded successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error reloading Mariadb"); + }); + }} + > + + {data?.applicationStatus === "idle" ? ( - + { + await start({ + mariadbId: mariadbId, + }) + .then(() => { + toast.success("Mariadb started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting Mariadb"); + }); + }} + > + + ) : ( - + { + await stop({ + mariadbId: mariadbId, + }) + .then(() => { + toast.success("Mariadb stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Mariadb"); + }); + }} + > + + )} { + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + refetch(); + }} + filteredLogs={filteredLogs} + />
); diff --git a/apps/dokploy/components/dashboard/mariadb/general/stop-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/stop-mariadb.tsx deleted file mode 100644 index 17eb4bcdc..000000000 --- a/apps/dokploy/components/dashboard/mariadb/general/stop-mariadb.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Ban } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mariadbId: string; -} - -export const StopMariadb = ({ mariadbId }: Props) => { - const { mutateAsync, isLoading } = api.mariadb.stop.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you absolutely sure to stop the database? - - - This will stop the database - - - - Cancel - { - await mutateAsync({ - mariadbId, - }) - .then(async () => { - await utils.mariadb.one.invalidate({ - mariadbId, - }); - toast.success("Application stopped succesfully"); - }) - .catch(() => { - toast.error("Error to stop the Application"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mariadb/start-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/start-mariadb.tsx deleted file mode 100644 index dff62cec3..000000000 --- a/apps/dokploy/components/dashboard/mariadb/start-mariadb.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { CheckCircle2 } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mariadbId: string; -} - -export const StartMariadb = ({ mariadbId }: Props) => { - const { mutateAsync, isLoading } = api.mariadb.start.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you sure to start the database? - - - This will start the database - - - - Cancel - { - await mutateAsync({ - mariadbId, - }) - .then(async () => { - await utils.mariadb.one.invalidate({ - mariadbId, - }); - toast.success("Database started succesfully"); - }) - .catch(() => { - toast.error("Error to start the Database"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx index 55b7e7e65..4c9be0903 100644 --- a/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/update-mariadb.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, SquarePen } from "lucide-react"; +import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -75,13 +75,13 @@ export const UpdateMariadb = ({ mariadbId }: Props) => { description: formData.description || "", }) .then(() => { - toast.success("MariaDB updated succesfully"); + toast.success("MariaDB updated successfully"); utils.mariadb.one.invalidate({ mariadbId: mariadbId, }); }) .catch(() => { - toast.error("Error to update the Mariadb"); + toast.error("Error updating the Mariadb"); }) .finally(() => {}); }; @@ -89,8 +89,12 @@ export const UpdateMariadb = ({ mariadbId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/mariadb/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/mariadb/volumes/show-volumes.tsx deleted file mode 100644 index a4a88bbe5..000000000 --- a/apps/dokploy/components/dashboard/mariadb/volumes/show-volumes.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { api } from "@/utils/api"; -import { AlertTriangle, Package } from "lucide-react"; -import React from "react"; -import { AddVolumes } from "../../application/advanced/volumes/add-volumes"; -import { DeleteVolume } from "../../application/advanced/volumes/delete-volume"; -import { UpdateVolume } from "../../application/advanced/volumes/update-volume"; -interface Props { - mariadbId: string; -} - -export const ShowVolumes = ({ mariadbId }: Props) => { - const { data, refetch } = api.mariadb.one.useQuery( - { - mariadbId, - }, - { enabled: !!mariadbId }, - ); - - return ( - - -
- Volumes - - If you want to persist data in this mariadb use the following config - to setup the volumes - -
- - {data && data?.mounts.length > 0 && ( - - Add Volume - - )} -
- - {data?.mounts.length === 0 ? ( -
- - - No volumes/mounts configured - - - Add Volume - -
- ) : ( -
- - Please remember to click Redeploy after adding, editing, or - deleting a mount to apply the changes. - -
- {data?.mounts.map((mount) => ( -
-
-
-
- Mount Type - - {mount.type.toUpperCase()} - -
- {mount.type === "volume" && ( -
- Volume Name - - {mount.volumeName} - -
- )} - - {mount.type === "file" && ( -
- Content - - {mount.content} - -
- )} - {mount.type === "bind" && ( -
- Host Path - - {mount.hostPath} - -
- )} -
- Mount Path - - {mount.mountPath} - -
-
-
- - -
-
-
- ))} -
-
- )} -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/advanced/show-mongo-advanced-settings.tsx b/apps/dokploy/components/dashboard/mongo/advanced/show-mongo-advanced-settings.tsx deleted file mode 100644 index cbb178f3e..000000000 --- a/apps/dokploy/components/dashboard/mongo/advanced/show-mongo-advanced-settings.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -import { ShowVolumes } from "../volumes/show-volumes"; -import { ShowMongoResources } from "./show-mongo-resources"; - -const addDockerImage = z.object({ - dockerImage: z.string().min(1, "Docker image is required"), - command: z.string(), -}); - -interface Props { - mongoId: string; -} - -type AddDockerImage = z.infer; -export const ShowAdvancedMongo = ({ mongoId }: Props) => { - const { data, refetch } = api.mongo.one.useQuery( - { - mongoId, - }, - { enabled: !!mongoId }, - ); - const { mutateAsync } = api.mongo.update.useMutation(); - - const form = useForm({ - defaultValues: { - dockerImage: "", - command: "", - }, - resolver: zodResolver(addDockerImage), - }); - - useEffect(() => { - if (data) { - form.reset({ - dockerImage: data.dockerImage, - command: data.command || "", - }); - } - }, [data, form, form.formState.isSubmitSuccessful, form.reset]); - - const onSubmit = async (formData: AddDockerImage) => { - await mutateAsync({ - mongoId, - dockerImage: formData?.dockerImage, - command: formData?.command, - }) - .then(async () => { - toast.success("Resources Updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error to Update the resources"); - }); - }; - return ( - <> -
- - - Advanced Settings - - -
- -
- ( - - Docker Image - - - - - - - )} - /> - ( - - Command - - - - - - - )} - /> -
-
- -
-
- -
-
- - -
- - ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/advanced/show-mongo-resources.tsx b/apps/dokploy/components/dashboard/mongo/advanced/show-mongo-resources.tsx deleted file mode 100644 index c18b54a8a..000000000 --- a/apps/dokploy/components/dashboard/mongo/advanced/show-mongo-resources.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addResourcesMongo = z.object({ - memoryReservation: z.number().nullable().optional(), - cpuLimit: z.number().nullable().optional(), - memoryLimit: z.number().nullable().optional(), - cpuReservation: z.number().nullable().optional(), -}); -interface Props { - mongoId: string; -} - -type AddResourcesMongo = z.infer; -export const ShowMongoResources = ({ mongoId }: Props) => { - const { data, refetch } = api.mongo.one.useQuery( - { - mongoId, - }, - { enabled: !!mongoId }, - ); - const { mutateAsync, isLoading } = api.mongo.update.useMutation(); - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(addResourcesMongo), - }); - - useEffect(() => { - if (data) { - form.reset({ - cpuLimit: data?.cpuLimit || undefined, - cpuReservation: data?.cpuReservation || undefined, - memoryLimit: data?.memoryLimit || undefined, - memoryReservation: data?.memoryReservation || undefined, - }); - } - }, [data, form, form.reset]); - - const onSubmit = async (formData: AddResourcesMongo) => { - await mutateAsync({ - mongoId, - cpuLimit: formData.cpuLimit || null, - cpuReservation: formData.cpuReservation || null, - memoryLimit: formData.memoryLimit || null, - memoryReservation: formData.memoryReservation || null, - }) - .then(async () => { - toast.success("Resources Updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error to Update the resources"); - }); - }; - return ( - - - Resources - - If you want to decrease or increase the resources to a specific. - application or database - - - - - Please remember to click Redeploy after modify the resources to apply - the changes. - -
- - Please remember to click Redeploy after modify the resources to - apply the changes. - - -
- ( - - Memory Reservation - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - - )} - /> - - { - return ( - - Memory Limit - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - - { - return ( - - Cpu Limit - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - { - return ( - - Cpu Reservation - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> -
-
- -
-
- -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/backups/show-backup-mongo.tsx b/apps/dokploy/components/dashboard/mongo/backups/show-backup-mongo.tsx deleted file mode 100644 index 0f0478c51..000000000 --- a/apps/dokploy/components/dashboard/mongo/backups/show-backup-mongo.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { api } from "@/utils/api"; -import { DatabaseBackup, Play } from "lucide-react"; -import Link from "next/link"; -import React from "react"; -import { toast } from "sonner"; -import { AddBackup } from "../../database/backups/add-backup"; -import { DeleteBackup } from "../../database/backups/delete-backup"; -import { UpdateBackup } from "../../database/backups/update-backup"; -interface Props { - mongoId: string; -} - -export const ShowBackupMongo = ({ mongoId }: Props) => { - const { data } = api.destination.all.useQuery(); - const { data: mongo, refetch: refetchMongo } = api.mongo.one.useQuery( - { - mongoId, - }, - { - enabled: !!mongoId, - }, - ); - - const { mutateAsync: manualBackup, isLoading: isManualBackup } = - api.backup.manualBackupMongo.useMutation(); - - return ( - - -
- Backups - - Add backups to your database to save the data to a different - provider. - -
- - {mongo && mongo?.backups?.length > 0 && ( - - )} -
- - {data?.length === 0 ? ( -
- - - To create a backup it is required to set at least 1 provider. - Please, go to{" "} - - Settings - {" "} - to do so. - -
- ) : ( -
- {mongo?.backups.length === 0 ? ( -
- - - No backups configured - - -
- ) : ( -
-
- {mongo?.backups.map((backup) => ( -
-
-
-
- Destination - - {backup.destination.name} - -
-
- Database - - {backup.database} - -
-
- Scheduled - - {backup.schedule} - -
-
- Prefix Storage - - {backup.prefix} - -
-
- Enabled - - {backup.enabled ? "Yes" : "No"} - -
-
-
- - - - - - Run Manual Backup - - - - -
-
-
- ))} -
-
- )} -
- )} -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/delete-mongo.tsx b/apps/dokploy/components/dashboard/mongo/delete-mongo.tsx deleted file mode 100644 index 86c2e2425..000000000 --- a/apps/dokploy/components/dashboard/mongo/delete-mongo.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const deleteMongoSchema = z.object({ - projectName: z.string().min(1, { - message: "Database name is required", - }), -}); - -type DeleteMongo = z.infer; - -interface Props { - mongoId: string; -} - -// commen - -export const DeleteMongo = ({ mongoId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.mongo.remove.useMutation(); - const { data } = api.mongo.one.useQuery({ mongoId }, { enabled: !!mongoId }); - const { push } = useRouter(); - const form = useForm({ - defaultValues: { - projectName: "", - }, - resolver: zodResolver(deleteMongoSchema), - }); - - const onSubmit = async (formData: DeleteMongo) => { - const expectedName = `${data?.name}/${data?.appName}`; - if (formData.projectName === expectedName) { - await mutateAsync({ mongoId }) - .then((data) => { - push(`/dashboard/project/${data?.projectId}`); - toast.success("Database deleted successfully"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error deleting the database"); - }); - } else { - form.setError("projectName", { - message: "Database name does not match", - }); - } - }; - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - database. If you are sure please enter the database name to delete - this database. - - -
-
- - ( - - - To confirm, type "{data?.name}/{data?.appName}" in the box - below - - - - - - - )} - /> - - -
- - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/environment/show-mongo-environment.tsx b/apps/dokploy/components/dashboard/mongo/environment/show-mongo-environment.tsx deleted file mode 100644 index 767967962..000000000 --- a/apps/dokploy/components/dashboard/mongo/environment/show-mongo-environment.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { CodeEditor } from "@/components/shared/code-editor"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; -import { Toggle } from "@/components/ui/toggle"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { EyeIcon, EyeOffIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addEnvironmentSchema = z.object({ - environment: z.string(), -}); - -type EnvironmentSchema = z.infer; - -interface Props { - mongoId: string; -} - -export const ShowMongoEnvironment = ({ mongoId }: Props) => { - const [isEnvVisible, setIsEnvVisible] = useState(true); - const { mutateAsync, isLoading } = api.mongo.saveEnvironment.useMutation(); - - const { data, refetch } = api.mongo.one.useQuery( - { - mongoId, - }, - { - enabled: !!mongoId, - }, - ); - const form = useForm({ - defaultValues: { - environment: "", - }, - resolver: zodResolver(addEnvironmentSchema), - }); - - useEffect(() => { - if (data) { - form.reset({ - environment: data.env || "", - }); - } - }, [form.reset, data, form]); - - const onSubmit = async (data: EnvironmentSchema) => { - mutateAsync({ - env: data.environment, - mongoId, - }) - .then(async () => { - toast.success("Environments Added"); - await refetch(); - }) - .catch(() => { - toast.error("Error to add environment"); - }); - }; - - return ( -
- - -
- Environment Settings - - You can add environment variables to your resource. - -
- - - {isEnvVisible ? ( - - ) : ( - - )} - -
- -
- - ( - - - - - - - - )} - /> - -
- -
- - -
-
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/general/deploy-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/deploy-mongo.tsx deleted file mode 100644 index 0ebda1ca6..000000000 --- a/apps/dokploy/components/dashboard/mongo/general/deploy-mongo.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { toast } from "sonner"; - -interface Props { - mongoId: string; -} - -export const DeployMongo = ({ mongoId }: Props) => { - const { data, refetch } = api.mongo.one.useQuery( - { - mongoId, - }, - { enabled: !!mongoId }, - ); - const { mutateAsync: deploy } = api.mongo.deploy.useMutation(); - const { mutateAsync: changeStatus } = api.mongo.changeStatus.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will deploy the mongo database - - - - Cancel - { - await changeStatus({ - mongoId, - applicationStatus: "running", - }) - .then(async () => { - toast.success("Deploying Database...."); - await refetch(); - await deploy({ - mongoId, - }).catch(() => { - toast.error("Error to deploy Database"); - }); - await refetch(); - }) - .catch((e) => { - toast.error(e.message || "Error to deploy Database"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/general/reset-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/reset-mongo.tsx deleted file mode 100644 index 19388bb7b..000000000 --- a/apps/dokploy/components/dashboard/mongo/general/reset-mongo.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { RefreshCcw } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mongoId: string; - appName: string; -} - -export const ResetMongo = ({ mongoId, appName }: Props) => { - const { refetch } = api.mongo.one.useQuery( - { - mongoId, - }, - { enabled: !!mongoId }, - ); - const { mutateAsync: reload, isLoading } = api.mongo.reload.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will reload the service - - - - Cancel - { - await reload({ - mongoId, - appName, - }) - .then(() => { - toast.success("Service Reloaded"); - }) - .catch(() => { - toast.error("Error to reload the service"); - }); - await refetch(); - }} - > - Confirm - - - - - ); -}; 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 7cfab289d..6dd2e9198 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 @@ -72,7 +72,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the external port"); + toast.error("Error saving the external port"); }); }; diff --git a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx index c8ae007af..df01e36d4 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx @@ -1,24 +1,61 @@ +import { DialogAction } from "@/components/shared/dialog-action"; +import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Terminal } from "lucide-react"; -import React from "react"; +import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; +import React, { useState } from "react"; +import { toast } from "sonner"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; -import { StartMongo } from "../start-mongo"; -import { DeployMongo } from "./deploy-mongo"; -import { ResetMongo } from "./reset-mongo"; -import { StopMongo } from "./stop-mongo"; interface Props { mongoId: string; } export const ShowGeneralMongo = ({ mongoId }: Props) => { - const { data } = api.mongo.one.useQuery( + const { data, refetch } = api.mongo.one.useQuery( { mongoId, }, { enabled: !!mongoId }, ); + + const { mutateAsync: reload, isLoading: isReloading } = + api.mongo.reload.useMutation(); + + const { mutateAsync: start, isLoading: isStarting } = + api.mongo.start.useMutation(); + + const { mutateAsync: stop, isLoading: isStopping } = + api.mongo.stop.useMutation(); + + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + api.mongo.deployWithLogs.useSubscription( + { + mongoId: mongoId, + }, + { + enabled: isDeploying, + onData(log) { + if (!isDrawerOpen) { + setIsDrawerOpen(true); + } + + if (log === "Deployment completed successfully!") { + setIsDeploying(false); + } + + const parsedLogs = parseLogs(log); + setFilteredLogs((prev) => [...prev, ...parsedLogs]); + }, + onError(error) { + console.error("Deployment logs error:", error); + setIsDeploying(false); + }, + }, + ); return ( <>
@@ -27,12 +64,92 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => { Deploy Settings - - + { + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); + }} + > + + + { + await reload({ + mongoId: mongoId, + appName: data?.appName || "", + }) + .then(() => { + toast.success("Mongo reloaded successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error reloading Mongo"); + }); + }} + > + + {data?.applicationStatus === "idle" ? ( - + { + await start({ + mongoId: mongoId, + }) + .then(() => { + toast.success("Mongo started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting Mongo"); + }); + }} + > + + ) : ( - + { + await stop({ + mongoId: mongoId, + }) + .then(() => { + toast.success("Mongo stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Mongo"); + }); + }} + > + + )} { + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + refetch(); + }} + filteredLogs={filteredLogs} + />
); diff --git a/apps/dokploy/components/dashboard/mongo/general/stop-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/stop-mongo.tsx deleted file mode 100644 index 09d2c372d..000000000 --- a/apps/dokploy/components/dashboard/mongo/general/stop-mongo.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Ban } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mongoId: string; -} - -export const StopMongo = ({ mongoId }: Props) => { - const { mutateAsync, isLoading } = api.mongo.stop.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you absolutely sure to stop the database? - - - This will stop the database - - - - Cancel - { - await mutateAsync({ - mongoId, - }) - .then(async () => { - await utils.mongo.one.invalidate({ - mongoId, - }); - toast.success("Application stopped succesfully"); - }) - .catch(() => { - toast.error("Error to stop the Application"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/start-mongo.tsx b/apps/dokploy/components/dashboard/mongo/start-mongo.tsx deleted file mode 100644 index 3bc30862a..000000000 --- a/apps/dokploy/components/dashboard/mongo/start-mongo.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { CheckCircle2 } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mongoId: string; -} - -export const StartMongo = ({ mongoId }: Props) => { - const { mutateAsync, isLoading } = api.mongo.start.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you sure to start the database? - - - This will start the database - - - - Cancel - { - await mutateAsync({ - mongoId, - }) - .then(async () => { - await utils.mongo.one.invalidate({ - mongoId, - }); - toast.success("Database started succesfully"); - }) - .catch(() => { - toast.error("Error to start the Database"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mongo/update-mongo.tsx b/apps/dokploy/components/dashboard/mongo/update-mongo.tsx index fdfd26397..c2e3616c4 100644 --- a/apps/dokploy/components/dashboard/mongo/update-mongo.tsx +++ b/apps/dokploy/components/dashboard/mongo/update-mongo.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, SquarePen } from "lucide-react"; +import { PenBoxIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -76,14 +76,14 @@ export const UpdateMongo = ({ mongoId }: Props) => { description: formData.description || "", }) .then(() => { - toast.success("Mongo updated succesfully"); + toast.success("Mongo updated successfully"); utils.mongo.one.invalidate({ mongoId: mongoId, }); setIsOpen(false); }) .catch(() => { - toast.error("Error to update mongo database"); + toast.error("Error updating mongo database"); }) .finally(() => {}); }; @@ -91,8 +91,12 @@ export const UpdateMongo = ({ mongoId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/mongo/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/mongo/volumes/show-volumes.tsx deleted file mode 100644 index c30f2df88..000000000 --- a/apps/dokploy/components/dashboard/mongo/volumes/show-volumes.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { api } from "@/utils/api"; -import { AlertTriangle, Package } from "lucide-react"; -import React from "react"; -import { AddVolumes } from "../../application/advanced/volumes/add-volumes"; -import { DeleteVolume } from "../../application/advanced/volumes/delete-volume"; -import { UpdateVolume } from "../../application/advanced/volumes/update-volume"; -interface Props { - mongoId: string; -} - -export const ShowVolumes = ({ mongoId }: Props) => { - const { data, refetch } = api.mongo.one.useQuery( - { - mongoId, - }, - { enabled: !!mongoId }, - ); - - return ( - - -
- Volumes - - If you want to persist data in this mongo use the following config. - to setup the volumes - -
- - {data && data?.mounts.length > 0 && ( - - Add Volume - - )} -
- - {data?.mounts.length === 0 ? ( -
- - - No volumes/mounts configured - - - Add Volume - -
- ) : ( -
- - Please remember to click Redeploy after adding, editing, or - deleting a mount to apply the changes. - -
- {data?.mounts.map((mount) => ( -
-
-
-
- Mount Type - - {mount.type.toUpperCase()} - -
- {mount.type === "volume" && ( -
- Volume Name - - {mount.volumeName} - -
- )} - - {mount.type === "file" && ( -
- Content - - {mount.content} - -
- )} - {mount.type === "bind" && ( -
- Host Path - - {mount.hostPath} - -
- )} -
- Mount Path - - {mount.mountPath} - -
-
-
- - -
-
-
- ))} -
-
- )} -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/monitoring/docker/show.tsx b/apps/dokploy/components/dashboard/monitoring/docker/show.tsx index d9deaa351..a457f35e6 100644 --- a/apps/dokploy/components/dashboard/monitoring/docker/show.tsx +++ b/apps/dokploy/components/dashboard/monitoring/docker/show.tsx @@ -187,81 +187,127 @@ export const DockerMonitoring = ({ return (
- - - Monitoring - - Watch the usage of your server in the current app. - - - -
-
-
- CPU - - Used: {currentData.cpu.value.toFixed(2)}% - - - -
-
- Memory - - {`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`} - - - -
- {appName === "dokploy" && ( -
- Space + +
+
+
+

+ Monitoring +

+

+ Watch the usage of your server in the current app +

+
+
+ +
+ + + CPU Usage + + +
- {`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`} + Used: {currentData.cpu.value.toFixed(2)}% - +
+
+
+ + + + + Memory Usage + + + +
+ + {`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`} + + +
- )} -
- Block I/O - - {`Read: ${currentData.block.value.readMb.toFixed( - 2, - )} MB / Write: ${currentData.block.value.writeMb.toFixed( - 3, - )} MB`} - - -
-
- Network - - {`In MB: ${currentData.network.value.inputMb.toFixed( - 2, - )} MB / Out MB: ${currentData.network.value.outputMb.toFixed( - 2, - )} MB`} - - -
-
+ + + + {appName === "dokploy" && ( + + + + Disk Space + + + +
+ + {`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`} + + + +
+
+
+ )} + + + + Block I/O + + +
+ + {`Read: ${currentData.block.value.readMb.toFixed( + 2, + )} MB / Write: ${currentData.block.value.writeMb.toFixed( + 3, + )} MB`} + + +
+
+
+ + + + + Network I/O + + + +
+ + {`In MB: ${currentData.network.value.inputMb.toFixed( + 2, + )} MB / Out MB: ${currentData.network.value.outputMb.toFixed( + 2, + )} MB`} + + +
+
+
- +
); diff --git a/apps/dokploy/components/dashboard/monitoring/web-server/show.tsx b/apps/dokploy/components/dashboard/monitoring/web-server/show.tsx index f6bd8cc29..d6f15057e 100644 --- a/apps/dokploy/components/dashboard/monitoring/web-server/show.tsx +++ b/apps/dokploy/components/dashboard/monitoring/web-server/show.tsx @@ -3,7 +3,7 @@ import { DockerMonitoring } from "../docker/show"; export const ShowMonitoring = () => { return ( -
+
); diff --git a/apps/dokploy/components/dashboard/mysql/advanced/show-mysql-advanced-settings.tsx b/apps/dokploy/components/dashboard/mysql/advanced/show-mysql-advanced-settings.tsx deleted file mode 100644 index 82198a0c5..000000000 --- a/apps/dokploy/components/dashboard/mysql/advanced/show-mysql-advanced-settings.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -import { ShowVolumes } from "../volumes/show-volumes"; -import { ShowMysqlResources } from "./show-mysql-resources"; - -const addDockerImage = z.object({ - dockerImage: z.string().min(1, "Docker image is required"), - command: z.string(), -}); - -interface Props { - mysqlId: string; -} - -type AddDockerImage = z.infer; -export const ShowAdvancedMysql = ({ mysqlId }: Props) => { - const { data, refetch } = api.mysql.one.useQuery( - { - mysqlId, - }, - { enabled: !!mysqlId }, - ); - const { mutateAsync } = api.mysql.update.useMutation(); - - const form = useForm({ - defaultValues: { - dockerImage: "", - command: "", - }, - resolver: zodResolver(addDockerImage), - }); - - useEffect(() => { - if (data) { - form.reset({ - dockerImage: data.dockerImage, - command: data.command || "", - }); - } - }, [data, form, form.reset]); - - const onSubmit = async (formData: AddDockerImage) => { - await mutateAsync({ - mysqlId, - dockerImage: formData?.dockerImage, - command: formData?.command, - }) - .then(async () => { - toast.success("Resources Updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error to Update the resources"); - }); - }; - return ( - <> -
- - - Advanced Settings - - -
- -
- ( - - Docker Image - - - - - - - )} - /> - ( - - Command - - - - - - - )} - /> -
-
- -
-
- -
-
- - -
- - ); -}; diff --git a/apps/dokploy/components/dashboard/mysql/advanced/show-mysql-resources.tsx b/apps/dokploy/components/dashboard/mysql/advanced/show-mysql-resources.tsx deleted file mode 100644 index 07ceeae9d..000000000 --- a/apps/dokploy/components/dashboard/mysql/advanced/show-mysql-resources.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addResourcesMysql = z.object({ - memoryReservation: z.number().nullable().optional(), - cpuLimit: z.number().nullable().optional(), - memoryLimit: z.number().nullable().optional(), - cpuReservation: z.number().nullable().optional(), -}); -interface Props { - mysqlId: string; -} - -type AddResourcesMysql = z.infer; -export const ShowMysqlResources = ({ mysqlId }: Props) => { - const { data, refetch } = api.mysql.one.useQuery( - { - mysqlId, - }, - { enabled: !!mysqlId }, - ); - const { mutateAsync, isLoading } = api.mysql.update.useMutation(); - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(addResourcesMysql), - }); - - useEffect(() => { - if (data) { - form.reset({ - cpuLimit: data?.cpuLimit || undefined, - cpuReservation: data?.cpuReservation || undefined, - memoryLimit: data?.memoryLimit || undefined, - memoryReservation: data?.memoryReservation || undefined, - }); - } - }, [data, form, form.reset]); - - const onSubmit = async (formData: AddResourcesMysql) => { - await mutateAsync({ - mysqlId, - cpuLimit: formData.cpuLimit || null, - cpuReservation: formData.cpuReservation || null, - memoryLimit: formData.memoryLimit || null, - memoryReservation: formData.memoryReservation || null, - }) - .then(async () => { - toast.success("Resources Updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error to Update the resources"); - }); - }; - return ( - - - Resources - - If you want to decrease or increase the resources to a specific. - application or database - - - - - Please remember to click Redeploy after modify the resources to apply - the changes. - -
- -
- ( - - Memory Reservation - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - - )} - /> - - { - return ( - - Memory Limit - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - - { - return ( - - Cpu Limit - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - { - return ( - - Cpu Reservation - - { - const value = e.target.value; - if (value === "") { - // Si el campo está vacío, establece el valor como null. - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - // Solo actualiza el valor si se convierte a un número válido. - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> -
-
- -
-
- -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mysql/backups/show-backup-mysql.tsx b/apps/dokploy/components/dashboard/mysql/backups/show-backup-mysql.tsx deleted file mode 100644 index a760fd929..000000000 --- a/apps/dokploy/components/dashboard/mysql/backups/show-backup-mysql.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { api } from "@/utils/api"; -import { DatabaseBackup, Play } from "lucide-react"; -import Link from "next/link"; -import React from "react"; -import { toast } from "sonner"; -import { AddBackup } from "../../database/backups/add-backup"; -import { DeleteBackup } from "../../database/backups/delete-backup"; -import { UpdateBackup } from "../../database/backups/update-backup"; -interface Props { - mysqlId: string; -} - -export const ShowBackupMySql = ({ mysqlId }: Props) => { - const { data } = api.destination.all.useQuery(); - const { data: mysql, refetch: refetchMySql } = api.mysql.one.useQuery( - { - mysqlId, - }, - { - enabled: !!mysqlId, - }, - ); - - const { mutateAsync: manualBackup, isLoading: isManualBackup } = - api.backup.manualBackupMySql.useMutation(); - - return ( - - -
- Backups - - Add backups to your database to save the data to a different - provider. - -
- - {mysql && mysql?.backups?.length > 0 && ( - - )} -
- - {data?.length === 0 ? ( -
- - - To create a backup it is required to set at least 1 provider. - Please, go to{" "} - - Settings - {" "} - to do so. - -
- ) : ( -
- {mysql?.backups.length === 0 ? ( -
- - - No backups configured - - -
- ) : ( -
-
- {mysql?.backups.map((backup) => ( -
-
-
-
- Destination - - {backup.destination.name} - -
-
- Database - - {backup.database} - -
-
- Scheduled - - {backup.schedule} - -
-
- Prefix Storage - - {backup.prefix} - -
-
- Enabled - - {backup.enabled ? "Yes" : "No"} - -
-
-
- - - - - - Run Manual Backup - - - - -
-
-
- ))} -
-
- )} -
- )} -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mysql/delete-mysql.tsx b/apps/dokploy/components/dashboard/mysql/delete-mysql.tsx deleted file mode 100644 index a887adc5a..000000000 --- a/apps/dokploy/components/dashboard/mysql/delete-mysql.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const deleteMysqlSchema = z.object({ - projectName: z.string().min(1, { - message: "Database name is required", - }), -}); - -type DeleteMysql = z.infer; - -interface Props { - mysqlId: string; -} - -export const DeleteMysql = ({ mysqlId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.mysql.remove.useMutation(); - const { data } = api.mysql.one.useQuery({ mysqlId }, { enabled: !!mysqlId }); - const { push } = useRouter(); - const form = useForm({ - defaultValues: { - projectName: "", - }, - resolver: zodResolver(deleteMysqlSchema), - }); - - const onSubmit = async (formData: DeleteMysql) => { - const expectedName = `${data?.name}/${data?.appName}`; - if (formData.projectName === expectedName) { - await mutateAsync({ mysqlId }) - .then((data) => { - push(`/dashboard/project/${data?.projectId}`); - toast.success("Database deleted successfully"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error deleting the database"); - }); - } else { - form.setError("projectName", { - message: "Database name does not match", - }); - } - }; - - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - database. If you are sure please enter the database name to delete - this database. - - -
-
- - ( - - - To confirm, type "{data?.name}/{data?.appName}" in the box - below - - - - - - - )} - /> - - -
- - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mysql/environment/show-mysql-environment.tsx b/apps/dokploy/components/dashboard/mysql/environment/show-mysql-environment.tsx deleted file mode 100644 index 036a63ab5..000000000 --- a/apps/dokploy/components/dashboard/mysql/environment/show-mysql-environment.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { CodeEditor } from "@/components/shared/code-editor"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; -import { Toggle } from "@/components/ui/toggle"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { EyeIcon, EyeOffIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addEnvironmentSchema = z.object({ - environment: z.string(), -}); - -type EnvironmentSchema = z.infer; - -interface Props { - mysqlId: string; -} - -export const ShowMysqlEnvironment = ({ mysqlId }: Props) => { - const [isEnvVisible, setIsEnvVisible] = useState(true); - const { mutateAsync, isLoading } = api.mysql.saveEnvironment.useMutation(); - - const { data, refetch } = api.mysql.one.useQuery( - { - mysqlId, - }, - { - enabled: !!mysqlId, - }, - ); - const form = useForm({ - defaultValues: { - environment: "", - }, - resolver: zodResolver(addEnvironmentSchema), - }); - - useEffect(() => { - if (data) { - form.reset({ - environment: data.env || "", - }); - } - }, [form.reset, data, form]); - - const onSubmit = async (data: EnvironmentSchema) => { - mutateAsync({ - env: data.environment, - mysqlId, - }) - .then(async () => { - toast.success("Environments Added"); - await refetch(); - }) - .catch(() => { - toast.error("Error to add environment"); - }); - }; - - return ( -
- - -
- Environment Settings - - You can add environment variables to your resource. - -
- - - {isEnvVisible ? ( - - ) : ( - - )} - -
- -
- - ( - - - - - - - - )} - /> - -
- -
- - -
-
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/mysql/general/deploy-mysql.tsx b/apps/dokploy/components/dashboard/mysql/general/deploy-mysql.tsx deleted file mode 100644 index a773feffc..000000000 --- a/apps/dokploy/components/dashboard/mysql/general/deploy-mysql.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { toast } from "sonner"; - -interface Props { - mysqlId: string; -} - -export const DeployMysql = ({ mysqlId }: Props) => { - const { data, refetch } = api.mysql.one.useQuery( - { - mysqlId, - }, - { enabled: !!mysqlId }, - ); - const { mutateAsync: deploy } = api.mysql.deploy.useMutation(); - const { mutateAsync: changeStatus } = api.mysql.changeStatus.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will deploy the mysql database - - - - Cancel - { - await changeStatus({ - mysqlId, - applicationStatus: "running", - }) - .then(async () => { - toast.success("Deploying Database...."); - await refetch(); - await deploy({ - mysqlId, - }).catch(() => { - toast.error("Error to deploy Database"); - }); - await refetch(); - }) - .catch((e) => { - toast.error(e.message || "Error to deploy Database"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mysql/general/reset-mysql.tsx b/apps/dokploy/components/dashboard/mysql/general/reset-mysql.tsx deleted file mode 100644 index 8b64ca9a1..000000000 --- a/apps/dokploy/components/dashboard/mysql/general/reset-mysql.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { RefreshCcw } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mysqlId: string; - appName: string; -} - -export const ResetMysql = ({ mysqlId, appName }: Props) => { - const { refetch } = api.mysql.one.useQuery( - { - mysqlId, - }, - { enabled: !!mysqlId }, - ); - const { mutateAsync: reload, isLoading } = api.mysql.reload.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will reload the service - - - - Cancel - { - await reload({ - mysqlId, - appName, - }) - .then(() => { - toast.success("Service Reloaded"); - }) - .catch(() => { - toast.error("Error to reload the service"); - }); - await refetch(); - }} - > - Confirm - - - - - ); -}; 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 009c8c3a5..dc1ca3a7d 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 @@ -72,7 +72,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the external port"); + toast.error("Error saving the external port"); }); }; diff --git a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx index f9928cb1d..56a191ceb 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-general-mysql.tsx @@ -1,24 +1,59 @@ +import { DialogAction } from "@/components/shared/dialog-action"; +import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Terminal } from "lucide-react"; -import React from "react"; +import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; +import React, { useState } from "react"; +import { toast } from "sonner"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; -import { StartMysql } from "../start-mysql"; -import { DeployMysql } from "./deploy-mysql"; -import { ResetMysql } from "./reset-mysql"; -import { StopMysql } from "./stop-mysql"; interface Props { mysqlId: string; } export const ShowGeneralMysql = ({ mysqlId }: Props) => { - const { data } = api.mysql.one.useQuery( + const { data, refetch } = api.mysql.one.useQuery( { mysqlId, }, { enabled: !!mysqlId }, ); + + const { mutateAsync: reload, isLoading: isReloading } = + api.mysql.reload.useMutation(); + const { mutateAsync: start, isLoading: isStarting } = + api.mysql.start.useMutation(); + + const { mutateAsync: stop, isLoading: isStopping } = + api.mysql.stop.useMutation(); + + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + api.mysql.deployWithLogs.useSubscription( + { + mysqlId: mysqlId, + }, + { + enabled: isDeploying, + onData(log) { + if (!isDrawerOpen) { + setIsDrawerOpen(true); + } + + if (log === "Deployment completed successfully!") { + setIsDeploying(false); + } + const parsedLogs = parseLogs(log); + setFilteredLogs((prev) => [...prev, ...parsedLogs]); + }, + onError(error) { + console.error("Deployment logs error:", error); + setIsDeploying(false); + }, + }, + ); return ( <>
@@ -27,12 +62,91 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => { Deploy Settings - - + { + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); + }} + > + + + { + await reload({ + mysqlId: mysqlId, + appName: data?.appName || "", + }) + .then(() => { + toast.success("Mysql reloaded successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error reloading Mysql"); + }); + }} + > + + {data?.applicationStatus === "idle" ? ( - + { + await start({ + mysqlId: mysqlId, + }) + .then(() => { + toast.success("Mysql started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting Mysql"); + }); + }} + > + + ) : ( - + { + await stop({ + mysqlId: mysqlId, + }) + .then(() => { + toast.success("Mysql stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Mysql"); + }); + }} + > + + )} { + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + refetch(); + }} + filteredLogs={filteredLogs} + />
); diff --git a/apps/dokploy/components/dashboard/mysql/general/stop-mysql.tsx b/apps/dokploy/components/dashboard/mysql/general/stop-mysql.tsx deleted file mode 100644 index 3cb0d681c..000000000 --- a/apps/dokploy/components/dashboard/mysql/general/stop-mysql.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Ban } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mysqlId: string; -} - -export const StopMysql = ({ mysqlId }: Props) => { - const { mutateAsync, isLoading } = api.mysql.stop.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you absolutely sure to stop the database? - - - This will stop the database - - - - Cancel - { - await mutateAsync({ - mysqlId, - }) - .then(async () => { - await utils.mysql.one.invalidate({ - mysqlId, - }); - toast.success("Application stopped succesfully"); - }) - .catch(() => { - toast.error("Error to stop the Application"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mysql/start-mysql.tsx b/apps/dokploy/components/dashboard/mysql/start-mysql.tsx deleted file mode 100644 index 60ebc08ba..000000000 --- a/apps/dokploy/components/dashboard/mysql/start-mysql.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { CheckCircle2 } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - mysqlId: string; -} - -export const StartMysql = ({ mysqlId }: Props) => { - const { mutateAsync, isLoading } = api.mysql.start.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you sure to start the database? - - - This will start the database - - - - Cancel - { - await mutateAsync({ - mysqlId, - }) - .then(async () => { - await utils.mysql.one.invalidate({ - mysqlId, - }); - toast.success("Database started succesfully"); - }) - .catch(() => { - toast.error("Error to start the Database"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx index 37b71b6cc..645575cdc 100644 --- a/apps/dokploy/components/dashboard/mysql/update-mysql.tsx +++ b/apps/dokploy/components/dashboard/mysql/update-mysql.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, SquarePen } from "lucide-react"; +import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -75,13 +75,13 @@ export const UpdateMysql = ({ mysqlId }: Props) => { description: formData.description || "", }) .then(() => { - toast.success("MySQL updated succesfully"); + toast.success("MySQL updated successfully"); utils.mysql.one.invalidate({ mysqlId: mysqlId, }); }) .catch(() => { - toast.error("Error to update the MySQL"); + toast.error("Error updating MySQL"); }) .finally(() => {}); }; @@ -89,8 +89,12 @@ export const UpdateMysql = ({ mysqlId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/mysql/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/mysql/volumes/show-volumes.tsx deleted file mode 100644 index 530a0f6a1..000000000 --- a/apps/dokploy/components/dashboard/mysql/volumes/show-volumes.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { api } from "@/utils/api"; -import { AlertTriangle, Package } from "lucide-react"; -import React from "react"; -import { AddVolumes } from "../../application/advanced/volumes/add-volumes"; -import { DeleteVolume } from "../../application/advanced/volumes/delete-volume"; -import { UpdateVolume } from "../../application/advanced/volumes/update-volume"; -interface Props { - mysqlId: string; -} - -export const ShowVolumes = ({ mysqlId }: Props) => { - const { data, refetch } = api.mysql.one.useQuery( - { - mysqlId, - }, - { enabled: !!mysqlId }, - ); - - return ( - - -
- Volumes - - If you want to persist data in this mysql use the following config - to setup the volumes - -
- - {data && data?.mounts.length > 0 && ( - - Add Volume - - )} -
- - {data?.mounts.length === 0 ? ( -
- - - No volumes/mounts configured - - - Add Volume - -
- ) : ( -
- - Please remember to click Redeploy after adding, editing, or - deleting a mount to apply the changes. - -
- {data?.mounts.map((mount) => ( -
-
-
-
- Mount Type - - {mount.type.toUpperCase()} - -
- {mount.type === "volume" && ( -
- Volume Name - - {mount.volumeName} - -
- )} - - {mount.type === "file" && ( -
- Content - - {mount.content} - -
- )} - {mount.type === "bind" && ( -
- Host Path - - {mount.hostPath} - -
- )} -
- Mount Path - - {mount.mountPath} - -
-
-
- - -
-
-
- ))} -
-
- )} -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/postgres/advanced/show-postgres-advanced-settings.tsx b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx similarity index 64% rename from apps/dokploy/components/dashboard/postgres/advanced/show-postgres-advanced-settings.tsx rename to apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx index c8002fa26..6e912db95 100644 --- a/apps/dokploy/components/dashboard/postgres/advanced/show-postgres-advanced-settings.tsx +++ b/apps/dokploy/components/dashboard/postgres/advanced/show-custom-command.tsx @@ -15,8 +15,7 @@ import React, { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -import { ShowVolumes } from "../volumes/show-volumes"; -import { ShowPostgresResources } from "./show-postgres-resources"; +import type { ServiceType } from "../../application/advanced/show-resources"; const addDockerImage = z.object({ dockerImage: z.string().min(1, "Docker image is required"), @@ -24,18 +23,39 @@ const addDockerImage = z.object({ }); interface Props { - postgresId: string; + id: string; + type: Exclude; } type AddDockerImage = z.infer; -export const ShowAdvancedPostgres = ({ postgresId }: Props) => { - const { data, refetch } = api.postgres.one.useQuery( - { - postgresId, - }, - { enabled: !!postgresId }, - ); - const { mutateAsync } = api.postgres.update.useMutation(); +export const ShowCustomCommand = ({ id, type }: Props) => { + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync, isLoading } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); const form = useForm({ defaultValues: { @@ -56,16 +76,20 @@ export const ShowAdvancedPostgres = ({ postgresId }: Props) => { const onSubmit = async (formData: AddDockerImage) => { await mutateAsync({ - postgresId, + mongoId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", dockerImage: formData?.dockerImage, command: formData?.command, }) .then(async () => { - toast.success("Resources Updated"); + toast.success("Custom Command Updated"); await refetch(); }) .catch(() => { - toast.error("Error to Update the resources"); + toast.error("Error updating the custom command"); }); }; return ( @@ -120,8 +144,6 @@ export const ShowAdvancedPostgres = ({ postgresId }: Props) => { - -
); diff --git a/apps/dokploy/components/dashboard/postgres/advanced/show-postgres-resources.tsx b/apps/dokploy/components/dashboard/postgres/advanced/show-postgres-resources.tsx deleted file mode 100644 index 7f224ee2a..000000000 --- a/apps/dokploy/components/dashboard/postgres/advanced/show-postgres-resources.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addResourcesPostgres = z.object({ - memoryReservation: z.number().nullable().optional(), - cpuLimit: z.number().nullable().optional(), - memoryLimit: z.number().nullable().optional(), - cpuReservation: z.number().nullable().optional(), -}); -interface Props { - postgresId: string; -} - -type AddResourcesPostgres = z.infer; -export const ShowPostgresResources = ({ postgresId }: Props) => { - const { data, refetch } = api.postgres.one.useQuery( - { - postgresId, - }, - { enabled: !!postgresId }, - ); - const { mutateAsync, isLoading } = api.postgres.update.useMutation(); - const form = useForm({ - defaultValues: {}, - resolver: zodResolver(addResourcesPostgres), - }); - - useEffect(() => { - if (data) { - form.reset({ - cpuLimit: data?.cpuLimit || undefined, - cpuReservation: data?.cpuReservation || undefined, - memoryLimit: data?.memoryLimit || undefined, - memoryReservation: data?.memoryReservation || undefined, - }); - } - }, [data, form, form.reset]); - - const onSubmit = async (formData: AddResourcesPostgres) => { - await mutateAsync({ - postgresId, - cpuLimit: formData.cpuLimit || null, - cpuReservation: formData.cpuReservation || null, - memoryLimit: formData.memoryLimit || null, - memoryReservation: formData.memoryReservation || null, - }) - .then(async () => { - toast.success("Resources Updated"); - await refetch(); - }) - .catch(() => { - toast.error("Error to Update the resources"); - }); - }; - return ( - - - Resources - - If you want to decrease or increase the resources to a specific. - application or database - - - - - Please remember to click Redeploy after modify the resources to apply - the changes. - -
- -
- ( - - Memory Reservation - - { - const value = e.target.value; - if (value === "") { - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - field.onChange(number); - } - } - }} - /> - - - - - )} - /> - - { - return ( - - Memory Limit - - { - const value = e.target.value; - if (value === "") { - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - - { - return ( - - Cpu Limit - - { - const value = e.target.value; - if (value === "") { - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> - { - return ( - - Cpu Reservation - - { - const value = e.target.value; - if (value === "") { - field.onChange(null); - } else { - const number = Number.parseInt(value, 10); - if (!Number.isNaN(number)) { - field.onChange(number); - } - } - }} - /> - - - - ); - }} - /> -
-
- -
-
- -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/postgres/delete-postgres.tsx b/apps/dokploy/components/dashboard/postgres/delete-postgres.tsx deleted file mode 100644 index 1767831a5..000000000 --- a/apps/dokploy/components/dashboard/postgres/delete-postgres.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const deletePostgresSchema = z.object({ - projectName: z.string().min(1, { - message: "Database name is required", - }), -}); - -type DeletePostgres = z.infer; - -interface Props { - postgresId: string; -} - -export const DeletePostgres = ({ postgresId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.postgres.remove.useMutation(); - const { data } = api.postgres.one.useQuery( - { postgresId }, - { enabled: !!postgresId }, - ); - const { push } = useRouter(); - const form = useForm({ - defaultValues: { - projectName: "", - }, - resolver: zodResolver(deletePostgresSchema), - }); - - const onSubmit = async (formData: DeletePostgres) => { - const expectedName = `${data?.name}/${data?.appName}`; - if (formData.projectName === expectedName) { - await mutateAsync({ postgresId }) - .then((data) => { - push(`/dashboard/project/${data?.projectId}`); - toast.success("Database deleted successfully"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error deleting the database"); - }); - } else { - form.setError("projectName", { - message: "Database name does not match", - }); - } - }; - - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - database. If you are sure please enter the database name to delete - this database. - - -
-
- - ( - - - To confirm, type "{data?.name}/{data?.appName}" in the box - below - - - - - - - )} - /> - - -
- - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/postgres/environment/show-postgres-environment.tsx b/apps/dokploy/components/dashboard/postgres/environment/show-postgres-environment.tsx deleted file mode 100644 index b66776959..000000000 --- a/apps/dokploy/components/dashboard/postgres/environment/show-postgres-environment.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { CodeEditor } from "@/components/shared/code-editor"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/components/ui/form"; -import { Toggle } from "@/components/ui/toggle"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { EyeIcon, EyeOffIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const addEnvironmentSchema = z.object({ - environment: z.string(), -}); - -type EnvironmentSchema = z.infer; - -interface Props { - postgresId: string; -} - -export const ShowPostgresEnvironment = ({ postgresId }: Props) => { - const [isEnvVisible, setIsEnvVisible] = useState(true); - const { mutateAsync, isLoading } = api.postgres.saveEnvironment.useMutation(); - - const { data, refetch } = api.postgres.one.useQuery( - { - postgresId, - }, - { - enabled: !!postgresId, - }, - ); - const form = useForm({ - defaultValues: { - environment: "", - }, - resolver: zodResolver(addEnvironmentSchema), - }); - - useEffect(() => { - if (data) { - form.reset({ - environment: data.env || "", - }); - } - }, [form.reset, data, form]); - - const onSubmit = async (data: EnvironmentSchema) => { - mutateAsync({ - env: data.environment, - postgresId, - }) - .then(async () => { - toast.success("Environments Added"); - await refetch(); - }) - .catch(() => { - toast.error("Error to add environment"); - }); - }; - - return ( -
- - -
- Environment Settings - - You can add environment variables to your resource. - -
- - - {isEnvVisible ? ( - - ) : ( - - )} - -
- -
- - ( - - - - - - - - )} - /> - -
- -
- - -
-
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/postgres/general/deploy-postgres.tsx b/apps/dokploy/components/dashboard/postgres/general/deploy-postgres.tsx deleted file mode 100644 index e329d9f80..000000000 --- a/apps/dokploy/components/dashboard/postgres/general/deploy-postgres.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { toast } from "sonner"; - -interface Props { - postgresId: string; -} - -export const DeployPostgres = ({ postgresId }: Props) => { - const { data, refetch } = api.postgres.one.useQuery( - { - postgresId, - }, - { enabled: !!postgresId }, - ); - const { mutateAsync: deploy } = api.postgres.deploy.useMutation(); - const { mutateAsync: changeStatus } = api.postgres.changeStatus.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will deploy the postgres database - - - - Cancel - { - await changeStatus({ - postgresId, - applicationStatus: "running", - }) - .then(async () => { - toast.success("Deploying Database...."); - await refetch(); - await deploy({ - postgresId, - }).catch(() => { - toast.error("Error to deploy Database"); - }); - await refetch(); - }) - .catch((e) => { - toast.error(e.message || "Error to deploy Database"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/postgres/general/reset-postgres.tsx b/apps/dokploy/components/dashboard/postgres/general/reset-postgres.tsx deleted file mode 100644 index 359c2b773..000000000 --- a/apps/dokploy/components/dashboard/postgres/general/reset-postgres.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { RefreshCcw } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - postgresId: string; - appName: string; -} - -export const ResetPostgres = ({ postgresId, appName }: Props) => { - const { refetch } = api.postgres.one.useQuery( - { - postgresId, - }, - { enabled: !!postgresId }, - ); - const { mutateAsync: reload, isLoading } = api.postgres.reload.useMutation(); - - return ( - - - - - - - Are you absolutely sure? - - This will reload the service - - - - Cancel - { - await reload({ - postgresId, - appName, - }) - .then(() => { - toast.success("Service Reloaded"); - }) - .catch(() => { - toast.error("Error to reload the service"); - }); - await refetch(); - }} - > - Confirm - - - - - ); -}; 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 e1b4369a6..e8fff7dca 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 @@ -74,7 +74,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => { await refetch(); }) .catch(() => { - toast.error("Error to save the external port"); + toast.error("Error saving the external port"); }); }; diff --git a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx index 781080ee5..43c3f4322 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-general-postgres.tsx @@ -1,41 +1,155 @@ +import { DialogAction } from "@/components/shared/dialog-action"; +import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Terminal } from "lucide-react"; -import React from "react"; +import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react"; +import React, { useState } from "react"; +import { toast } from "sonner"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; -import { StartPostgres } from "../start-postgres"; -import { DeployPostgres } from "./deploy-postgres"; -import { ResetPostgres } from "./reset-postgres"; -import { StopPostgres } from "./stop-postgres"; interface Props { postgresId: string; } export const ShowGeneralPostgres = ({ postgresId }: Props) => { - const { data } = api.postgres.one.useQuery( + const { data, refetch } = api.postgres.one.useQuery( { - postgresId, + postgresId: postgresId, }, { enabled: !!postgresId }, ); + const { mutateAsync: reload, isLoading: isReloading } = + api.postgres.reload.useMutation(); + + const { mutateAsync: stop, isLoading: isStopping } = + api.postgres.stop.useMutation(); + + const { mutateAsync: start, isLoading: isStarting } = + api.postgres.start.useMutation(); + + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + api.postgres.deployWithLogs.useSubscription( + { + postgresId: postgresId, + }, + { + enabled: isDeploying, + onData(log) { + if (!isDrawerOpen) { + setIsDrawerOpen(true); + } + + if (log === "Deployment completed successfully!") { + setIsDeploying(false); + } + const parsedLogs = parseLogs(log); + setFilteredLogs((prev) => [...prev, ...parsedLogs]); + }, + onError(error) { + console.error("Deployment logs error:", error); + setIsDeploying(false); + }, + }, + ); + return (
- - Deploy Settings + + General - - - + + { + setIsDeploying(true); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); + }} + > + + + + { + await reload({ + postgresId: postgresId, + appName: data?.appName || "", + }) + .then(() => { + toast.success("Postgres reloaded successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error reloading Postgres"); + }); + }} + > + + {data?.applicationStatus === "idle" ? ( - + { + await start({ + postgresId: postgresId, + }) + .then(() => { + toast.success("Postgres started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting Postgres"); + }); + }} + > + + ) : ( - + { + await stop({ + postgresId: postgresId, + }) + .then(() => { + toast.success("Postgres stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Postgres"); + }); + }} + > + + )} { + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + refetch(); + }} + filteredLogs={filteredLogs} + />
); }; diff --git a/apps/dokploy/components/dashboard/postgres/general/stop-postgres.tsx b/apps/dokploy/components/dashboard/postgres/general/stop-postgres.tsx deleted file mode 100644 index 9bf1738f0..000000000 --- a/apps/dokploy/components/dashboard/postgres/general/stop-postgres.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { Ban } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - postgresId: string; -} - -export const StopPostgres = ({ postgresId }: Props) => { - const { mutateAsync, isLoading } = api.postgres.stop.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you absolutely sure to stop the database? - - - This will stop the database - - - - Cancel - { - await mutateAsync({ - postgresId, - }) - .then(async () => { - await utils.postgres.one.invalidate({ - postgresId, - }); - toast.success("Application stopped succesfully"); - }) - .catch(() => { - toast.error("Error to stop the Application"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/postgres/start-postgres.tsx b/apps/dokploy/components/dashboard/postgres/start-postgres.tsx deleted file mode 100644 index 8d9450826..000000000 --- a/apps/dokploy/components/dashboard/postgres/start-postgres.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { api } from "@/utils/api"; -import { CheckCircle2 } from "lucide-react"; -import { toast } from "sonner"; - -interface Props { - postgresId: string; -} - -export const StartPostgres = ({ postgresId }: Props) => { - const { mutateAsync, isLoading } = api.postgres.start.useMutation(); - const utils = api.useUtils(); - return ( - - - - - - - - Are you sure to start the database? - - - This will start the database - - - - Cancel - { - await mutateAsync({ - postgresId, - }) - .then(async () => { - await utils.postgres.one.invalidate({ - postgresId, - }); - toast.success("Database started succesfully"); - }) - .catch(() => { - toast.error("Error to start the Database"); - }); - }} - > - Confirm - - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx index dcc3e9b12..54ad5bce0 100644 --- a/apps/dokploy/components/dashboard/postgres/update-postgres.tsx +++ b/apps/dokploy/components/dashboard/postgres/update-postgres.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle, SquarePen } from "lucide-react"; +import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -76,14 +76,14 @@ export const UpdatePostgres = ({ postgresId }: Props) => { description: formData.description || "", }) .then(() => { - toast.success("Postgres updated succesfully"); + toast.success("Postgres updated successfully"); utils.postgres.one.invalidate({ postgresId: postgresId, }); setIsOpen(false); }) .catch(() => { - toast.error("Error to update the postgres"); + toast.error("Error updating Postgres"); }) .finally(() => {}); }; @@ -91,8 +91,12 @@ export const UpdatePostgres = ({ postgresId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/postgres/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/postgres/volumes/show-volumes.tsx deleted file mode 100644 index 9fd33c505..000000000 --- a/apps/dokploy/components/dashboard/postgres/volumes/show-volumes.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { api } from "@/utils/api"; -import { AlertTriangle, Package } from "lucide-react"; -import React from "react"; -import { AddVolumes } from "../../application/advanced/volumes/add-volumes"; -import { DeleteVolume } from "../../application/advanced/volumes/delete-volume"; -import { UpdateVolume } from "../../application/advanced/volumes/update-volume"; -interface Props { - postgresId: string; -} - -export const ShowVolumes = ({ postgresId }: Props) => { - const { data, refetch } = api.postgres.one.useQuery( - { - postgresId, - }, - { enabled: !!postgresId }, - ); - - return ( - - -
- Volumes - - If you want to persist data in this postgres database use the - following config to setup the volumes - -
- - {data && data?.mounts.length > 0 && ( - - Add Volume - - )} -
- - {data?.mounts.length === 0 ? ( -
- - - No volumes/mounts configured - - - Add Volume - -
- ) : ( -
- - Please remember to click Redeploy after adding, editing, or - deleting a mount to apply the changes. - -
- {data?.mounts.map((mount) => ( -
-
- {/* */} -
-
- Mount Type - - {mount.type.toUpperCase()} - -
- {mount.type === "volume" && ( -
- Volume Name - - {mount.volumeName} - -
- )} - - {mount.type === "file" && ( -
- Content - - {mount.content} - -
- )} - {mount.type === "bind" && ( -
- Host Path - - {mount.hostPath} - -
- )} -
- Mount Path - - {mount.mountPath} - -
-
-
- - -
-
-
- ))} -
-
- )} -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index 2ecacdf63..da30cfee3 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -70,7 +70,7 @@ interface Props { export const AddApplication = ({ projectId, projectName }: Props) => { const utils = api.useUtils(); - + const { data: isCloud } = api.settings.isCloud.useQuery(); const [visible, setVisible] = useState(false); const slug = slugify(projectName); const { data: servers } = api.server.withSSHKey.useQuery(); @@ -104,7 +104,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { }); }) .catch((e) => { - toast.error("Error to create the service"); + toast.error("Error creating the service"); }); }; @@ -166,7 +166,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { - Select a Server (Optional) + Select a Server {!isCloud ? "(Optional)" : ""} @@ -176,7 +176,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { side="top" > - If not server is selected, the application will be + If no server is selected, the application will be deployed on the server where the user is logged in. @@ -197,7 +197,12 @@ export const AddApplication = ({ projectId, projectName }: Props) => { key={server.serverId} value={server.serverId} > - {server.name} + + {server.name} + + {server.ipAddress} + + ))} Servers ({servers?.length}) @@ -213,7 +218,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { name="appName" render={({ field }) => ( - AppName + App Name @@ -229,7 +234,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { Description