mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
7 Commits
migration/
...
v0.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfdc73f8d1 | ||
|
|
64ada7020a | ||
|
|
4706adc0c0 | ||
|
|
e01d92d1d9 | ||
|
|
fe22890311 | ||
|
|
2b7c7632f4 | ||
|
|
1b7244e841 |
@@ -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
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [siumauricio]
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: #
|
||||
open_collective: dokploy
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report
|
||||
labels: ["bug"]
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -11,27 +11,18 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: |
|
||||
A detailed, step-by-step description of how to reproduce the issue is required.
|
||||
Please ensure your report includes clear instructions using numbered lists.
|
||||
|
||||
If possible, provide a link to a repository or project where the issue can be reproduced.
|
||||
description: A step-by-step description of how to reproduce the issue, or a link to the reproducible repository.
|
||||
placeholder: |
|
||||
1. Create a application
|
||||
2. Click X
|
||||
3. Y will happen
|
||||
|
||||
Make sure to:
|
||||
- Use numbered lists to outline steps clearly.
|
||||
- Include all relevant commands and configurations.
|
||||
- Provide a link to a reproducible repository if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current vs. Expected behavior
|
||||
description: A clear and concise description of what the bug is, and what you expected to happen.
|
||||
placeholder: "Following the steps from the previous section, I expected A to happen, but I observed B instead"
|
||||
placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -54,23 +45,12 @@ body:
|
||||
label: Which area(s) are affected? (Select all that apply)
|
||||
multiple: true
|
||||
options:
|
||||
- "Installation"
|
||||
- "Application"
|
||||
- "Databases"
|
||||
- "Docker Compose"
|
||||
- "Traefik"
|
||||
- "Docker"
|
||||
- "Remote server"
|
||||
- "Local Development"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Are you deploying the applications where Dokploy is installed or on a remote server?
|
||||
options:
|
||||
- "Same server where Dokploy is installed"
|
||||
- "Remote server"
|
||||
- "Both"
|
||||
- 'Installation'
|
||||
- 'Application'
|
||||
- 'Databases'
|
||||
- 'Docker Compose'
|
||||
- 'Traefik'
|
||||
- 'Docker'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -79,16 +59,4 @@ body:
|
||||
description: |
|
||||
Any extra information that might help us investigate.
|
||||
placeholder: |
|
||||
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Will you send a PR to fix it?
|
||||
description: Let us know if you are planning to submit a pull request to address this issue.
|
||||
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
- "Maybe, need help"
|
||||
validations:
|
||||
required: true
|
||||
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
||||
15
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
15
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement to the project
|
||||
labels: ["enhancement"]
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -30,15 +30,4 @@ body:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Will you send a PR to implement it?
|
||||
description: Let us know if you are planning to submit a pull request to implement this feature.
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
- "Maybe, need help"
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
BIN
.github/sponsors/hostinger.jpg
vendored
BIN
.github/sponsors/hostinger.jpg
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB |
BIN
.github/sponsors/logo.png
vendored
BIN
.github/sponsors/logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
BIN
.github/sponsors/lxaer.png
vendored
BIN
.github/sponsors/lxaer.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 248 KiB |
BIN
.github/sponsors/mandarin.png
vendored
BIN
.github/sponsors/mandarin.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
BIN
.github/sponsors/startupfame.png
vendored
BIN
.github/sponsors/startupfame.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
80
.github/workflows/deploy.yml
vendored
80
.github/workflows/deploy.yml
vendored
@@ -1,80 +0,0 @@
|
||||
name: Build Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["canary", "main"]
|
||||
|
||||
jobs:
|
||||
build-and-push-cloud-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.cloud
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/cloud:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
NEXT_PUBLIC_UMAMI_HOST=${{ secrets.NEXT_PUBLIC_UMAMI_HOST }}
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
|
||||
|
||||
build-and-push-schedule-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.schedule
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
build-and-push-server-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.server
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
85
.github/workflows/pull-request.yml
vendored
85
.github/workflows/pull-request.yml
vendored
@@ -1,46 +1,59 @@
|
||||
name: Pull Request
|
||||
|
||||
name: Pull request
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, canary]
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
jobs:
|
||||
lint-and-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
build-app:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.18.0]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm typecheck
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Run Build
|
||||
run: pnpm build
|
||||
- name: Run Tests
|
||||
run: pnpm run test
|
||||
|
||||
build-and-test:
|
||||
needs: lint-and-typecheck
|
||||
build-and-push-docker-on-push:
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm build
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
parallel-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm test
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Prepare .env file
|
||||
run: |
|
||||
cp .env.production.example .env.production
|
||||
|
||||
- name: Build and push Docker image using custom script
|
||||
run: |
|
||||
chmod +x ./docker/push.sh
|
||||
./docker/push.sh ${{ github.ref_name == 'canary' && 'canary' || '' }}
|
||||
72
.gitignore
vendored
72
.gitignore
vendored
@@ -1,42 +1,58 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
/redis-data
|
||||
traefik.yml
|
||||
.docker
|
||||
.env.production
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
# testing
|
||||
/coverage
|
||||
/dist
|
||||
/production-server
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
/logs
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
/dokploy
|
||||
/config
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
# production
|
||||
/build
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Build Outputs
|
||||
.next/
|
||||
out/
|
||||
dist
|
||||
|
||||
|
||||
# Debug
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Editor
|
||||
.vscode
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# otros
|
||||
/.data
|
||||
/.main
|
||||
|
||||
*.lockb
|
||||
*.rdb
|
||||
.idea
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
128
CONTRIBUTING.md
128
CONTRIBUTING.md
@@ -1,7 +1,10 @@
|
||||
|
||||
|
||||
# Contributing
|
||||
|
||||
Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute.
|
||||
|
||||
|
||||
Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues.
|
||||
|
||||
We have a few guidelines to follow when contributing to this project:
|
||||
@@ -14,12 +17,9 @@ 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.
|
||||
Before you craete a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
@@ -29,27 +29,28 @@ Before you create a Pull Request, please make sure your commit message follows t
|
||||
```
|
||||
|
||||
#### Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- **feat**: A new feature
|
||||
- **fix**: A bug fix
|
||||
- **docs**: Documentation only changes
|
||||
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
- **perf**: A code change that improves performance
|
||||
- **test**: Adding missing tests or correcting existing tests
|
||||
- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
- **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
|
||||
- **chore**: Other changes that don't modify `src` or `test` files
|
||||
- **revert**: Reverts a previous commit
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **docs**: Documentation only changes
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **perf**: A code change that improves performance
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
|
||||
* **chore**: Other changes that don't modify `src` or `test` files
|
||||
* **revert**: Reverts a previous commit
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
feat: add new feature
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||
@@ -58,50 +59,45 @@ Before you start, please make the clone based on the `canary` branch, since the
|
||||
git clone https://github.com/dokploy/dokploy.git
|
||||
cd dokploy
|
||||
pnpm install
|
||||
cp apps/dokploy/.env.example apps/dokploy/.env
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Is required to have **Docker** installed on your machine.
|
||||
|
||||
|
||||
### Setup
|
||||
|
||||
Run the command that will spin up all the required services and files.
|
||||
|
||||
```bash
|
||||
pnpm run dokploy:setup
|
||||
```
|
||||
|
||||
Run this script
|
||||
```bash
|
||||
pnpm run server:script
|
||||
pnpm run setup
|
||||
```
|
||||
|
||||
Now run the development server.
|
||||
|
||||
```bash
|
||||
pnpm run dokploy:dev
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
|
||||
Go to http://localhost:3000 to see the development server
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
pnpm run dokploy:build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
To build the docker image
|
||||
|
||||
```bash
|
||||
pnpm run docker:build
|
||||
```
|
||||
|
||||
To push the docker image
|
||||
|
||||
```bash
|
||||
pnpm run docker:push
|
||||
```
|
||||
@@ -142,6 +138,7 @@ curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
```
|
||||
|
||||
|
||||
## Pull Request
|
||||
|
||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||
@@ -155,6 +152,10 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.
|
||||
|
||||
Thank you for your contribution!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Templates
|
||||
|
||||
To add a new template, go to `templates` folder and create a new folder with the name of the template.
|
||||
@@ -169,47 +170,42 @@ Let's take the example of `plausible` template.
|
||||
```typescript
|
||||
// EXAMPLE
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
type DomainSchema,
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const mainDomain = generateRandomDomain(schema);
|
||||
const secretBase = generateBase64(64);
|
||||
const toptKeyBase = generateBase64(32);
|
||||
|
||||
const domains: DomainSchema[] = [
|
||||
{
|
||||
host: mainDomain,
|
||||
port: 8000,
|
||||
serviceName: "plausible",
|
||||
},
|
||||
];
|
||||
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const secretBase = generateBase64(64);
|
||||
const toptKeyBase = generateBase64(32);
|
||||
|
||||
const envs = [
|
||||
`BASE_URL=http://${mainDomain}`,
|
||||
`SECRET_KEY_BASE=${secretBase}`,
|
||||
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
const envs = [
|
||||
// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
||||
`PLAUSIBLE_HOST=${randomDomain}`,
|
||||
"PLAUSIBLE_PORT=8000",
|
||||
`BASE_URL=http://${randomDomain}`,
|
||||
`SECRET_KEY_BASE=${secretBase}`,
|
||||
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
||||
content: `some content......`,
|
||||
},
|
||||
];
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
||||
content: `some content......`,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
mounts,
|
||||
domains,
|
||||
};
|
||||
return {
|
||||
envs,
|
||||
mounts,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -237,14 +233,10 @@ export function generate(schema: Schema): Template {
|
||||
|
||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
||||
|
||||
### Recommendations
|
||||
|
||||
### Recomendations
|
||||
- Use the same name of the folder as the id of the template.
|
||||
- The logo should be in the public folder.
|
||||
- If you want to show a domain in the UI, please add the `_HOST` suffix at the end of the variable name.
|
||||
- If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
||||
- Test first on a vps or a server to make sure the template works.
|
||||
|
||||
## Docs & Website
|
||||
|
||||
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
|
||||
|
||||
|
||||
82
Dockerfile
82
Dockerfile
@@ -1,50 +1,51 @@
|
||||
# Etapa 1: Prepare image for building
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable && apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/dokploy run build
|
||||
|
||||
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
||||
|
||||
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
||||
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
|
||||
|
||||
FROM base AS dokploy
|
||||
WORKDIR /app
|
||||
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
# Copy package.json and pnpm-lock.yaml
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
# Install dependencies only for building
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
COPY --from=build /prod/dokploy/dist ./dist
|
||||
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
||||
COPY --from=build /prod/dokploy/public ./public
|
||||
COPY --from=build /prod/dokploy/package.json ./package.json
|
||||
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
||||
COPY .env.production ./.env
|
||||
COPY --from=build /prod/dokploy/components.json ./components.json
|
||||
COPY --from=build /prod/dokploy/node_modules ./node_modules
|
||||
# Copy the rest of the source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm run build
|
||||
|
||||
# Stage 2: Prepare image for production
|
||||
FROM node:18-slim AS production
|
||||
|
||||
# Install dependencies only for production
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable && apt-get update && apt-get install -y curl && apt-get install -y apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the rest of the source code
|
||||
COPY --from=base /app/.next ./.next
|
||||
COPY --from=base /app/dist ./dist
|
||||
COPY --from=base /app/next.config.mjs ./next.config.mjs
|
||||
COPY --from=base /app/public ./public
|
||||
COPY --from=base /app/package.json ./package.json
|
||||
COPY --from=base /app/drizzle ./drizzle
|
||||
COPY --from=base /app/.env.production ./.env
|
||||
COPY --from=base /app/components.json ./components.json
|
||||
|
||||
# Install dependencies only for production
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
# Install docker
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
|
||||
|
||||
# Install Nixpacks and tsx
|
||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||
@@ -53,8 +54,11 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& ./install.sh \
|
||||
&& pnpm install -g tsx
|
||||
|
||||
# Install buildpacks
|
||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
# Install buildpacks
|
||||
RUN curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
|
||||
CMD ["pnpm", "start"]
|
||||
@@ -1,61 +0,0 @@
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||
|
||||
|
||||
# Deploy only the dokploy app
|
||||
ARG NEXT_PUBLIC_UMAMI_HOST
|
||||
ENV NEXT_PUBLIC_UMAMI_HOST=$NEXT_PUBLIC_UMAMI_HOST
|
||||
|
||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
|
||||
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/dokploy run build
|
||||
|
||||
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
||||
|
||||
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
||||
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
|
||||
|
||||
FROM base AS dokploy
|
||||
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/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
COPY --from=build /prod/dokploy/dist ./dist
|
||||
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
||||
COPY --from=build /prod/dokploy/public ./public
|
||||
COPY --from=build /prod/dokploy/package.json ./package.json
|
||||
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
||||
COPY --from=build /prod/dokploy/components.json ./components.json
|
||||
COPY --from=build /prod/dokploy/node_modules ./node_modules
|
||||
|
||||
|
||||
# Install RCLONE
|
||||
RUN curl https://rclone.org/install.sh | bash
|
||||
|
||||
# tsx
|
||||
RUN pnpm install -g tsx
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
@@ -1,36 +0,0 @@
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/schedules run build
|
||||
|
||||
RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
|
||||
|
||||
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
|
||||
|
||||
FROM base AS dokploy
|
||||
WORKDIR /app
|
||||
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/schedules/dist ./dist
|
||||
COPY --from=build /prod/schedules/package.json ./package.json
|
||||
COPY --from=build /prod/schedules/node_modules ./node_modules
|
||||
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||
@@ -1,36 +0,0 @@
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/api run build
|
||||
|
||||
RUN pnpm --filter=./apps/api --prod deploy /prod/api
|
||||
|
||||
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
|
||||
|
||||
FROM base AS dokploy
|
||||
WORKDIR /app
|
||||
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/api/dist ./dist
|
||||
COPY --from=build /prod/api/package.json ./package.json
|
||||
COPY --from=build /prod/api/node_modules ./node_modules
|
||||
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
||||
|
||||
## Additional Terms for Specific Features
|
||||
|
||||
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
||||
|
||||
For further inquiries or permissions, please contact us directly.
|
||||
|
||||
57
README-de.md
Normal file
57
README-de.md
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
</div>
|
||||
|
||||
<div align="center" style="width:100%;">
|
||||
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Reflex Logo" style="width:60%;">
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
|
||||
Dokploy ist eine kostenlose und self-hostable Platform as a Service (PaaS), welche das hosten und managen von deinen Projekten und Datenbanken vereinfacht, das geschieht mithilfe von Docker und Treafik. Es ist designt, um deine Leistung und die Sicherheit deiner Projekte zu verbessern. Dokploy erlaubt dir schnell und einfach auf jeder VPS deine Projekte zu verwirklichen.
|
||||
|
||||
|
||||
## Erklärung
|
||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
|
||||
|
||||
|
||||
|
||||
|
||||
## 🌟 Vorteile
|
||||
|
||||
- **Projekte**: - **Projekte**: Hoste jegliche Art von Projekt (Node.js, PHP, Python, Go, Ruby, etc.) mit Einfachheit.
|
||||
- **Datenbanken**: Erstelle und manage Datenbanken, wie MySQL, PostgreSQL, MongoDB, MariaDB, Redis, und mehr.
|
||||
- **Docker Management**: Einfach Docker container hosten und managen.
|
||||
- **Traefik Integration**: Automatische Integration mit Traefik für routing und load balancing
|
||||
- **Real-time Monitoring**: Monitor von CPU, RAM, Speicher, und network Nutzung.
|
||||
- **Database Backups**: Automatische Backups mit Support für mehrere Speicher Systeme.
|
||||
|
||||
|
||||
## 🚀 Loslegen
|
||||
|
||||
Um anzufangen führe einfach den folgende command in einer VPS aus:
|
||||
|
||||
```bash
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
```
|
||||
|
||||
Getestete Systems:
|
||||
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 Dokumentation
|
||||
|
||||
Für eine detaillierte Dokumentation, siehe [docs.dokploy.com/docs](https://docs.dokploy.com)
|
||||
|
||||
56
README-ru.md
Normal file
56
README-ru.md
Normal file
@@ -0,0 +1,56 @@
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
</div>
|
||||
|
||||
<div align="center" style="width:100%;">
|
||||
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Логотип Dokploy" style="width:60%;">
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
|
||||
|
||||
Dokploy - это бесплатная самоустанавливаемая Платформа как Сервис (PaaS), которая упрощает развертывание и управление приложениями и базами данных с использованием Docker и Traefik. Разработанный для повышения эффективности и безопасности, Dokploy позволяет развертывать ваши приложения на любом VPS.
|
||||
|
||||
|
||||
|
||||
## Объяснение
|
||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md)
|
||||
|
||||
|
||||
|
||||
|
||||
## 🌟 Особенности
|
||||
|
||||
- **Приложения**: Легко развертывать любой тип приложения (Node.js, PHP, Python, Go, Ruby и др.).
|
||||
- **Базы данных**: Создавайте и управляйте базами данных с поддержкой MySQL, PostgreSQL, MongoDB, MariaDB, Redis и других.
|
||||
- **Управление Docker**: Легко развертывать и управляйте контейнерами Docker.
|
||||
- **Интеграция с Traefik**: Автоматически интегрируется с Traefik для маршрутизации и балансировки нагрузки.
|
||||
- **Мониторинг в реальном времени**: Отслеживайте использование CPU, памяти, хранилища и сети.
|
||||
- **Резервное копирование баз данных**: Автоматизируйте резервное копирование с поддержкой нескольких мест хранения.
|
||||
|
||||
|
||||
## 🚀 Начало работы
|
||||
|
||||
Чтобы установить, выполните следующую команду на VPS:
|
||||
|
||||
|
||||
```bash
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
```
|
||||
|
||||
Проверенные системы:
|
||||
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 Документация
|
||||
Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com).
|
||||
60
README-zh.md
Normal file
60
README-zh.md
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
</div>
|
||||
|
||||
<div align="center" style="width:100%;">
|
||||
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Reflex Logo" style="width:60%;">
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
|
||||
|
||||
Dokploy 是一个免费的自托管平台即服务 (PaaS),它使用 Docker 和 Traefik 简化了应用程序和数据库的部署和管理。 Dokploy 旨在提高效率和安全性,允许您在任何 VPS 上部署应用程序。
|
||||
|
||||
## 语言
|
||||
[English](README.md)
|
||||
|
||||
[中文](README-zh.md)
|
||||
|
||||
[Deutsch](README-de.md)
|
||||
|
||||
[Русский Язык](README-ru.md)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 🌟 功能
|
||||
|
||||
- **应用程序**: 轻松部署任何类型的应用程序(Node.js,PHP,Python,Go、Ruby 等)。数据库: 创建和管理数据库,支持 MySQL,PostgreSQL,MongoDB、MariaDB、Redis 等。
|
||||
- **Docker 管理**: 轻松部署和管理 Docker 容器。
|
||||
- **Traefik 集成**: 自动与 Traefik 集成,用于路由和负载均衡。
|
||||
- **实时监控**: 监控 CPU,内存,存储和网络使用情况。
|
||||
- **数据库备份**: 支持多种存储目的地自动备份。
|
||||
|
||||
## 🚀 入门
|
||||
要开始使用 请在VPS 上运行以下命令:
|
||||
|
||||
```bash
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
```
|
||||
|
||||
经过测试的系统:
|
||||
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 文档
|
||||
|
||||
如需查看详细的文档资料 请访问[docs.dokploy.com/docs](https://docs.dokploy.com)
|
||||
142
README.md
142
README.md
@@ -1,138 +1,94 @@
|
||||
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
<div>
|
||||
<a href="https://dokploy.com" target="_blank" rel="noopener">
|
||||
<img style="object-fit: cover;" align="center" width="100%"src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." />
|
||||
</a>
|
||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
|
||||
|
||||
</div>
|
||||
|
||||
</br>
|
||||
<div align="center">
|
||||
<div>Join us on Discord for help, feedback, and discussions!</div>
|
||||
</br>
|
||||
<a href="https://discord.gg/2tBnJ3jDJc">
|
||||
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
Dokploy includes multiple features to make your life easier.
|
||||
Dokploy include multiples features to make your life easier.
|
||||
|
||||
|
||||
* **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||
* **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
||||
* **Backups**: Automate backups for databases to a external storage destination.
|
||||
* **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||
* **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
|
||||
* **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
|
||||
* **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
* **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
||||
* **Docker Management**: Easily deploy and manage Docker containers.
|
||||
* **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
||||
* **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
|
||||
|
||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
|
||||
- **Backups**: Automate backups for databases to an external storage destination.
|
||||
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
||||
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
|
||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
|
||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||
- **CLI/API**: Manage your applications and databases using the command line or through the API.
|
||||
- **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
|
||||
- **Multi Server**: Deploy and manage your applications remotely to external servers.
|
||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
To get started, run the following command on a VPS:
|
||||
To get started run the following command in a VPS:
|
||||
|
||||
Want to skip the installation process? [Try the Dokploy Cloud](https://app.dokploy.com).
|
||||
|
||||
```bash
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
```
|
||||
|
||||
|
||||
## 📄 Documentation
|
||||
|
||||
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
## Sponsors
|
||||
|
||||
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
|
||||
## Video Tutorial
|
||||
<a href="https://youtu.be/mznYKPvhcfw">
|
||||
<img src="https://dokploy.com/banner.webp" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||
</a>
|
||||
|
||||
[Dokploy Open Collective](https://opencollective.com/dokploy)
|
||||
|
||||
[Github Sponsors](https://github.com/sponsors/Siumauricio)
|
||||
## Donations
|
||||
|
||||
<!-- Hero Sponsors 🎖 -->
|
||||
If you like dokploy, and want to support the project to cover the costs of hosting, testing and development new features, you can donate to the project using the following link:
|
||||
|
||||
<!-- Add Hero Sponsors here -->
|
||||
Thanks to all the supporters!
|
||||
|
||||
### Hero Sponsors 🎖
|
||||
https://opencollective.com/dokploy
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
||||
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
||||
</a>
|
||||
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 10px;">
|
||||
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
||||
</a>
|
||||
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Premium Supporters 🥇
|
||||
<a href="https://opencollective.com/dokploy"><img src="https://opencollective.com/dokploy/individuals.svg?width=890"></a>
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
|
||||
</div>
|
||||
|
||||
<!-- Elite Contributors 🥈 -->
|
||||
|
||||
<!-- Add Elite Contributors here -->
|
||||
|
||||
### Supporting Members 🥉
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
||||
</div>
|
||||
|
||||
### Community Backers 🤝
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
||||
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
|
||||
|
||||
</div>
|
||||
|
||||
#### Organizations:
|
||||
|
||||
[](https://opencollective.com/dokploy)
|
||||
|
||||
#### Individuals:
|
||||
|
||||
[](https://opencollective.com/dokploy)
|
||||
|
||||
### Contributors 🤝
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
|
||||
</a>
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
<a href="https://youtu.be/mznYKPvhcfw">
|
||||
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||
</a>
|
||||
|
||||
<!-- ## Supported OS
|
||||
|
||||
- Ubuntu 24.04 LTS
|
||||
|
||||
## Support OS
|
||||
|
||||
- Ubuntu 24.04 LTS
|
||||
- Ubuntu 23.10
|
||||
- Ubuntu 22.04 LTS
|
||||
- Ubuntu 20.04 LTS
|
||||
- Ubuntu 22.04 LTS
|
||||
- Ubuntu 20.04 LTS
|
||||
- Ubuntu 18.04 LTS
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8 -->
|
||||
- Centos 8
|
||||
|
||||
|
||||
|
||||
## Explanation
|
||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { addSuffixToAllProperties } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { load } from "js-yaml";
|
||||
import { addPrefixToAllProperties } from "@/server/utils/docker/compose";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
|
||||
const composeFile1 = `
|
||||
version: "3.8"
|
||||
@@ -119,11 +119,11 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all properties in compose file 1", () => {
|
||||
test("Add prefix to all properties in compose file 1", () => {
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile1);
|
||||
});
|
||||
@@ -242,11 +242,11 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all properties in compose file 2", () => {
|
||||
test("Add prefix to all properties in compose file 2", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile2);
|
||||
});
|
||||
@@ -365,11 +365,11 @@ secrets:
|
||||
file: ./service_secret.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all properties in compose file 3", () => {
|
||||
test("Add prefix to all properties in compose file 3", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile3);
|
||||
});
|
||||
@@ -466,11 +466,11 @@ volumes:
|
||||
driver: local
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all properties in Plausible compose file", () => {
|
||||
test("Add prefix to all properties in Plausible compose file", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToConfigsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToConfigsRoot } from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -23,19 +23,19 @@ configs:
|
||||
file: ./web-config.yml
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in root property", () => {
|
||||
test("Add prefix to configs in root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.configs) {
|
||||
return;
|
||||
}
|
||||
const configs = addSuffixToConfigsRoot(composeData.configs, suffix);
|
||||
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
|
||||
|
||||
expect(configs).toBeDefined();
|
||||
for (const configKey of Object.keys(configs)) {
|
||||
expect(configKey).toContain(`-${suffix}`);
|
||||
expect(configKey).toContain(`-${prefix}`);
|
||||
expect(configs[configKey]).toBeDefined();
|
||||
}
|
||||
});
|
||||
@@ -59,23 +59,23 @@ configs:
|
||||
file: ./another-config.yml
|
||||
`;
|
||||
|
||||
test("Add suffix to multiple configs in root property", () => {
|
||||
test("Add prefix to multiple configs in root property", () => {
|
||||
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.configs) {
|
||||
return;
|
||||
}
|
||||
const configs = addSuffixToConfigsRoot(composeData.configs, suffix);
|
||||
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
|
||||
|
||||
expect(configs).toBeDefined();
|
||||
for (const configKey of Object.keys(configs)) {
|
||||
expect(configKey).toContain(`-${suffix}`);
|
||||
expect(configKey).toContain(`-${prefix}`);
|
||||
expect(configs[configKey]).toBeDefined();
|
||||
}
|
||||
expect(configs).toHaveProperty(`web-config-${suffix}`);
|
||||
expect(configs).toHaveProperty(`another-config-${suffix}`);
|
||||
expect(configs).toHaveProperty(`web-config-${prefix}`);
|
||||
expect(configs).toHaveProperty(`another-config-${prefix}`);
|
||||
});
|
||||
|
||||
const composeFileDifferentProperties = `
|
||||
@@ -92,25 +92,25 @@ configs:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add suffix to configs with different properties in root property", () => {
|
||||
test("Add prefix to configs with different properties in root property", () => {
|
||||
const composeData = load(
|
||||
composeFileDifferentProperties,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.configs) {
|
||||
return;
|
||||
}
|
||||
const configs = addSuffixToConfigsRoot(composeData.configs, suffix);
|
||||
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
|
||||
|
||||
expect(configs).toBeDefined();
|
||||
for (const configKey of Object.keys(configs)) {
|
||||
expect(configKey).toContain(`-${suffix}`);
|
||||
expect(configKey).toContain(`-${prefix}`);
|
||||
expect(configs[configKey]).toBeDefined();
|
||||
}
|
||||
expect(configs).toHaveProperty(`web-config-${suffix}`);
|
||||
expect(configs).toHaveProperty(`special-config-${suffix}`);
|
||||
expect(configs).toHaveProperty(`web-config-${prefix}`);
|
||||
expect(configs).toHaveProperty(`special-config-${prefix}`);
|
||||
});
|
||||
|
||||
const composeFileConfigRoot = `
|
||||
@@ -162,15 +162,15 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs in root property", () => {
|
||||
test("Add prefix to configs in root property", () => {
|
||||
const composeData = load(composeFileConfigRoot) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData?.configs) {
|
||||
return;
|
||||
}
|
||||
const configs = addSuffixToConfigsRoot(composeData.configs, suffix);
|
||||
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
|
||||
const updatedComposeData = { ...composeData, configs };
|
||||
|
||||
// Verificar que el resultado coincide con el archivo esperado
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToConfigsInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToConfigsInServices } from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -19,19 +19,19 @@ configs:
|
||||
file: ./web-config.yml
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services", () => {
|
||||
test("Add prefix to configs in services", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addSuffixToConfigsInServices(composeData.services, suffix);
|
||||
const services = addPrefixToConfigsInServices(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData.services?.web?.configs).toContainEqual({
|
||||
source: `web-config-${suffix}`,
|
||||
source: `web-config-${prefix}`,
|
||||
target: "/etc/nginx/nginx.conf",
|
||||
});
|
||||
});
|
||||
@@ -51,17 +51,17 @@ configs:
|
||||
file: ./web-config.yml
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services with single config", () => {
|
||||
test("Add prefix to configs in services with single config", () => {
|
||||
const composeData = load(
|
||||
composeFileSingleServiceConfig,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addSuffixToConfigsInServices(composeData.services, suffix);
|
||||
const services = addPrefixToConfigsInServices(composeData.services, prefix);
|
||||
|
||||
expect(services).toBeDefined();
|
||||
for (const serviceKey of Object.keys(services)) {
|
||||
@@ -69,7 +69,7 @@ test("Add suffix to configs in services with single config", () => {
|
||||
if (serviceConfigs) {
|
||||
for (const config of serviceConfigs) {
|
||||
if (typeof config === "object") {
|
||||
expect(config.source).toContain(`-${suffix}`);
|
||||
expect(config.source).toContain(`-${prefix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,17 +105,17 @@ configs:
|
||||
file: ./common-config.yml
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services with multiple configs", () => {
|
||||
test("Add prefix to configs in services with multiple configs", () => {
|
||||
const composeData = load(
|
||||
composeFileMultipleServicesConfigs,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addSuffixToConfigsInServices(composeData.services, suffix);
|
||||
const services = addPrefixToConfigsInServices(composeData.services, prefix);
|
||||
|
||||
expect(services).toBeDefined();
|
||||
for (const serviceKey of Object.keys(services)) {
|
||||
@@ -123,7 +123,7 @@ test("Add suffix to configs in services with multiple configs", () => {
|
||||
if (serviceConfigs) {
|
||||
for (const config of serviceConfigs) {
|
||||
if (typeof config === "object") {
|
||||
expect(config.source).toContain(`-${suffix}`);
|
||||
expect(config.source).toContain(`-${prefix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,17 +179,17 @@ services:
|
||||
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs in services", () => {
|
||||
test("Add prefix to configs in services", () => {
|
||||
const composeData = load(composeFileConfigServices) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToConfigsInServices(
|
||||
const updatedComposeData = addPrefixToConfigsInServices(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addPrefixToAllConfigs,
|
||||
addPrefixToConfigsRoot,
|
||||
} from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -77,12 +80,12 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all configs in root and services", () => {
|
||||
test("Add prefix to all configs in root and services", () => {
|
||||
const composeData = load(composeFileCombinedConfigs) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllConfigs(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileCombinedConfigs);
|
||||
});
|
||||
@@ -159,14 +162,14 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs with environment and external", () => {
|
||||
test("Add prefix to configs with environment and external", () => {
|
||||
const composeData = load(
|
||||
composeFileWithEnvAndExternal,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllConfigs(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileWithEnvAndExternal);
|
||||
});
|
||||
@@ -231,14 +234,14 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs with template driver and labels", () => {
|
||||
test("Add prefix to configs with template driver and labels", () => {
|
||||
const composeData = load(
|
||||
composeFileWithTemplateDriverAndLabels,
|
||||
) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllConfigs(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(
|
||||
expectedComposeFileWithTemplateDriverAndLabels,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -35,19 +35,19 @@ test("Generate random hash with 8 characters", () => {
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
test("Add suffix to networks root property", () => {
|
||||
test("Add prefix to networks root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const volumeKey of Object.keys(networks)) {
|
||||
expect(volumeKey).toContain(`-${suffix}`);
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,19 +79,19 @@ networks:
|
||||
internal: true
|
||||
`;
|
||||
|
||||
test("Add suffix to advanced networks root property (2 TRY)", () => {
|
||||
test("Add prefix to advanced networks root property (2 TRY)", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain(`-${suffix}`);
|
||||
expect(networkKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -120,19 +120,19 @@ networks:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with external properties", () => {
|
||||
test("Add prefix to networks with external properties", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain(`-${suffix}`);
|
||||
expect(networkKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -160,19 +160,19 @@ networks:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with IPAM configurations", () => {
|
||||
test("Add prefix to networks with IPAM configurations", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain(`-${suffix}`);
|
||||
expect(networkKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -201,19 +201,19 @@ networks:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with custom options", () => {
|
||||
test("Add prefix to networks with custom options", () => {
|
||||
const composeData = load(composeFile5) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain(`-${suffix}`);
|
||||
expect(networkKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -240,7 +240,7 @@ networks:
|
||||
external: true
|
||||
`;
|
||||
|
||||
// Expected compose file with static suffix `testhash`
|
||||
// Expected compose file with static prefix `testhash`
|
||||
const expectedComposeFile6 = `
|
||||
version: "3.8"
|
||||
|
||||
@@ -264,70 +264,18 @@ networks:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with static suffix", () => {
|
||||
test("Add prefix to networks with static prefix", () => {
|
||||
const composeData = load(composeFile6) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
|
||||
|
||||
const expectedComposeData = load(
|
||||
expectedComposeFile6,
|
||||
) as ComposeSpecification;
|
||||
expect(networks).toStrictEqual(expectedComposeData.networks);
|
||||
});
|
||||
|
||||
const composeFile7 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
`;
|
||||
|
||||
const expectedComposeFile7 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.driver.mtu: 1200
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
external_network:
|
||||
external: true
|
||||
name: dokploy-network
|
||||
`;
|
||||
test("It shoudn't add suffix to dokploy-network", () => {
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
|
||||
expect(networks).toBeDefined();
|
||||
for (const networkKey of Object.keys(networks)) {
|
||||
expect(networkKey).toContain("dokploy-network");
|
||||
}
|
||||
});
|
||||
181
__test__/compose/network/network-service.test.ts
Normal file
181
__test__/compose/network/network-service.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNetworks } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
- backend
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToServiceNetworks(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData?.services?.web?.networks).toContain(
|
||||
`frontend-${prefix}`,
|
||||
);
|
||||
|
||||
expect(actualComposeData?.services?.api?.networks).toContain(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
|
||||
const apiNetworks = actualComposeData?.services?.api?.networks;
|
||||
|
||||
expect(apiNetworks).toBeDefined();
|
||||
expect(actualComposeData?.services?.api?.networks).toContain(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Caso 2: Objeto con aliases
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- api
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services with aliases", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToServiceNetworks(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData.services?.api?.networks).toHaveProperty(
|
||||
`frontend-${prefix}`,
|
||||
);
|
||||
|
||||
const networkConfig =
|
||||
actualComposeData?.services?.api?.networks[`frontend-${prefix}`];
|
||||
expect(networkConfig).toBeDefined();
|
||||
expect(networkConfig?.aliases).toContain("api");
|
||||
|
||||
expect(actualComposeData.services?.api?.networks).not.toHaveProperty(
|
||||
"frontend-ash",
|
||||
);
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
backend:
|
||||
|
||||
networks:
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services (Object with simple networks)", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToServiceNetworks(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData.services?.redis?.networks).toHaveProperty(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
});
|
||||
|
||||
const composeFileCombined = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- api
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
backend:
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add prefix to networks in services (combined case)", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addPrefixToServiceNetworks(composeData.services, prefix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
// Caso 1: ListOfStrings
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`frontend-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
|
||||
// Caso 2: Objeto con aliases
|
||||
const apiNetworks = actualComposeData.services?.api?.networks;
|
||||
expect(apiNetworks).toHaveProperty(`frontend-${prefix}`);
|
||||
expect(apiNetworks[`frontend-${prefix}`]).toBeDefined();
|
||||
expect(apiNetworks).not.toHaveProperty("frontend");
|
||||
|
||||
// Caso 3: Objeto con redes simples
|
||||
const redisNetworks = actualComposeData.services?.redis?.networks;
|
||||
expect(redisNetworks).toHaveProperty(`backend-${prefix}`);
|
||||
expect(redisNetworks).not.toHaveProperty("backend");
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addSuffixToAllNetworks,
|
||||
addSuffixToServiceNetworks,
|
||||
} from "@dokploy/server";
|
||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
addPrefixToAllNetworks,
|
||||
addPrefixToServiceNetworks,
|
||||
} from "@/server/utils/docker/compose/network";
|
||||
import { addPrefixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -38,54 +38,52 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services and root (combined case)", () => {
|
||||
test("Add prefix to networks in services and root (combined case)", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
// Prefijo para redes definidas en el root
|
||||
if (composeData.networks) {
|
||||
composeData.networks = addSuffixToNetworksRoot(
|
||||
composeData.networks = addPrefixToNetworksRoot(
|
||||
composeData.networks,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
// Prefijo para redes definidas en los servicios
|
||||
if (composeData.services) {
|
||||
composeData.services = addSuffixToServiceNetworks(
|
||||
composeData.services = addPrefixToServiceNetworks(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
const actualComposeData = { ...composeData };
|
||||
|
||||
// Verificar redes en root
|
||||
expect(actualComposeData.networks).toHaveProperty(`frontend-${suffix}`);
|
||||
expect(actualComposeData.networks).toHaveProperty(`backend-${suffix}`);
|
||||
expect(actualComposeData.networks).toHaveProperty(`frontend-${prefix}`);
|
||||
expect(actualComposeData.networks).toHaveProperty(`backend-${prefix}`);
|
||||
expect(actualComposeData.networks).not.toHaveProperty("frontend");
|
||||
expect(actualComposeData.networks).not.toHaveProperty("backend");
|
||||
|
||||
// Caso 1: ListOfStrings
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`frontend-${suffix}`,
|
||||
`frontend-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`backend-${suffix}`,
|
||||
`backend-${prefix}`,
|
||||
);
|
||||
|
||||
// Caso 2: Objeto con aliases
|
||||
const apiNetworks = actualComposeData.services?.api?.networks as {
|
||||
[key: string]: { aliases?: string[] };
|
||||
};
|
||||
expect(apiNetworks).toHaveProperty(`frontend-${suffix}`);
|
||||
expect(apiNetworks?.[`frontend-${suffix}`]?.aliases).toContain("api");
|
||||
const apiNetworks = actualComposeData.services?.api?.networks;
|
||||
expect(apiNetworks).toHaveProperty(`frontend-${prefix}`);
|
||||
expect(apiNetworks[`frontend-${prefix}`]?.aliases).toContain("api");
|
||||
expect(apiNetworks).not.toHaveProperty("frontend");
|
||||
|
||||
// Caso 3: Objeto con redes simples
|
||||
const redisNetworks = actualComposeData.services?.redis?.networks;
|
||||
expect(redisNetworks).toHaveProperty(`backend-${suffix}`);
|
||||
expect(redisNetworks).toHaveProperty(`backend-${prefix}`);
|
||||
expect(redisNetworks).not.toHaveProperty("backend");
|
||||
});
|
||||
|
||||
@@ -119,14 +117,14 @@ networks:
|
||||
driver: bridge
|
||||
`);
|
||||
|
||||
test("Add suffix to networks in compose file", () => {
|
||||
test("Add prefix to networks in compose file", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
if (!composeData?.networks) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile);
|
||||
});
|
||||
@@ -181,11 +179,11 @@ networks:
|
||||
driver: bridge
|
||||
`);
|
||||
|
||||
test("Add suffix to networks in compose file with external and internal networks", () => {
|
||||
test("Add prefix to networks in compose file with external and internal networks", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
const prefix = "testhash";
|
||||
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile2);
|
||||
});
|
||||
@@ -246,89 +244,11 @@ networks:
|
||||
com.docker.network.bridge.enable_icc: "true"
|
||||
`);
|
||||
|
||||
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
|
||||
test("Add prefix to networks in compose file with multiple services and complex network configurations", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
const prefix = "testhash";
|
||||
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile3);
|
||||
});
|
||||
|
||||
const composeFile4 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: myapp:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- app
|
||||
backend:
|
||||
dokploy-network:
|
||||
|
||||
worker:
|
||||
image: worker:latest
|
||||
networks:
|
||||
- backend
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.bridge.enable_icc: "true"
|
||||
|
||||
dokploy-network:
|
||||
driver: bridge
|
||||
|
||||
`;
|
||||
|
||||
const expectedComposeFile4 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: myapp:latest
|
||||
networks:
|
||||
frontend-testhash:
|
||||
aliases:
|
||||
- app
|
||||
backend-testhash:
|
||||
dokploy-network:
|
||||
|
||||
worker:
|
||||
image: worker:latest
|
||||
networks:
|
||||
- backend-testhash
|
||||
- dokploy-network
|
||||
|
||||
networks:
|
||||
frontend-testhash:
|
||||
driver: bridge
|
||||
attachable: true
|
||||
|
||||
backend-testhash:
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.bridge.enable_icc: "true"
|
||||
|
||||
dokploy-network:
|
||||
driver: bridge
|
||||
|
||||
|
||||
|
||||
`);
|
||||
|
||||
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile4);
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { load, dump } from "js-yaml";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { addPrefixToSecretsRoot } from "@/server/utils/docker/compose/secrets";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -23,18 +23,18 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in root property", () => {
|
||||
test("Add prefix to secrets in root property", () => {
|
||||
const composeData = load(composeFileSecretsRoot) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
return;
|
||||
}
|
||||
const secrets = addSuffixToSecretsRoot(composeData.secrets, suffix);
|
||||
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
|
||||
expect(secrets).toBeDefined();
|
||||
if (secrets) {
|
||||
for (const secretKey of Object.keys(secrets)) {
|
||||
expect(secretKey).toContain(`-${suffix}`);
|
||||
expect(secretKey).toContain(`-${prefix}`);
|
||||
expect(secrets[secretKey]).toBeDefined();
|
||||
}
|
||||
}
|
||||
@@ -52,19 +52,19 @@ secrets:
|
||||
file: ./api_key.txt
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in root property (Test 1)", () => {
|
||||
test("Add prefix to secrets in root property (Test 1)", () => {
|
||||
const composeData = load(composeFileSecretsRoot1) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
return;
|
||||
}
|
||||
const secrets = addSuffixToSecretsRoot(composeData.secrets, suffix);
|
||||
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
|
||||
expect(secrets).toBeDefined();
|
||||
|
||||
if (secrets) {
|
||||
for (const secretKey of Object.keys(secrets)) {
|
||||
expect(secretKey).toContain(`-${suffix}`);
|
||||
expect(secretKey).toContain(`-${prefix}`);
|
||||
expect(secrets[secretKey]).toBeDefined();
|
||||
}
|
||||
}
|
||||
@@ -84,19 +84,19 @@ secrets:
|
||||
external: true
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in root property (Test 2)", () => {
|
||||
test("Add prefix to secrets in root property (Test 2)", () => {
|
||||
const composeData = load(composeFileSecretsRoot2) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
return;
|
||||
}
|
||||
const secrets = addSuffixToSecretsRoot(composeData.secrets, suffix);
|
||||
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
|
||||
expect(secrets).toBeDefined();
|
||||
|
||||
if (secrets) {
|
||||
for (const secretKey of Object.keys(secrets)) {
|
||||
expect(secretKey).toContain(`-${suffix}`);
|
||||
expect(secretKey).toContain(`-${prefix}`);
|
||||
expect(secrets[secretKey]).toBeDefined();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToSecretsInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToSecretsInServices } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -18,22 +18,22 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services", () => {
|
||||
test("Add prefix to secrets in services", () => {
|
||||
const composeData = load(composeFileSecretsServices) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addSuffixToSecretsInServices(
|
||||
const updatedComposeData = addPrefixToSecretsInServices(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData.services?.db?.secrets).toContain(
|
||||
`db_password-${suffix}`,
|
||||
`db_password-${prefix}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -51,22 +51,22 @@ secrets:
|
||||
file: ./app_secret.txt
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services (Test 1)", () => {
|
||||
test("Add prefix to secrets in services (Test 1)", () => {
|
||||
const composeData = load(composeFileSecretsServices1) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addSuffixToSecretsInServices(
|
||||
const updatedComposeData = addPrefixToSecretsInServices(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData.services?.app?.secrets).toContain(
|
||||
`app_secret-${suffix}`,
|
||||
`app_secret-${prefix}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -90,24 +90,24 @@ secrets:
|
||||
file: ./frontend_secret.txt
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services (Test 2)", () => {
|
||||
test("Add prefix to secrets in services (Test 2)", () => {
|
||||
const composeData = load(composeFileSecretsServices2) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addSuffixToSecretsInServices(
|
||||
const updatedComposeData = addPrefixToSecretsInServices(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData.services?.backend?.secrets).toContain(
|
||||
`backend_secret-${suffix}`,
|
||||
`backend_secret-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.frontend?.secrets).toContain(
|
||||
`frontend_secret-${suffix}`,
|
||||
`frontend_secret-${prefix}`,
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { addSuffixToAllSecrets } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addPrefixToAllSecrets } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -47,11 +47,11 @@ secrets:
|
||||
file: ./app_secret.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets", () => {
|
||||
test("Add prefix to all secrets", () => {
|
||||
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets);
|
||||
});
|
||||
@@ -98,11 +98,11 @@ secrets:
|
||||
file: ./cache_secret.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets (3rd Case)", () => {
|
||||
test("Add prefix to all secrets (3rd Case)", () => {
|
||||
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets3);
|
||||
});
|
||||
@@ -149,11 +149,11 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets (4th Case)", () => {
|
||||
test("Add prefix to all secrets (4th Case)", () => {
|
||||
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets4);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -27,33 +27,33 @@ test("Generate random hash with 8 characters", () => {
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
test("Add suffix to service names with container_name in compose file", () => {
|
||||
test("Add prefix to service names with container_name in compose file", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que el nombre del contenedor ha cambiado correctamente
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.container_name).toBe(
|
||||
`web_container-${suffix}`,
|
||||
expect(actualComposeData.services[`web-${prefix}`].container_name).toBe(
|
||||
`web_container-${prefix}`,
|
||||
);
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -32,49 +32,49 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with depends_on (array) in compose file", () => {
|
||||
test("Add prefix to service names with depends_on (array) in compose file", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que los nombres en depends_on tienen el prefijo
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.depends_on).toContain(
|
||||
`db-${suffix}`,
|
||||
expect(actualComposeData.services[`web-${prefix}`].depends_on).toContain(
|
||||
`db-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.depends_on).toContain(
|
||||
`api-${suffix}`,
|
||||
expect(actualComposeData.services[`web-${prefix}`].depends_on).toContain(
|
||||
`api-${prefix}`,
|
||||
);
|
||||
|
||||
// Verificar que los servicios `db` y `api` también tienen el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("db");
|
||||
expect(actualComposeData.services?.[`db-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`db-${prefix}`].image).toBe(
|
||||
"postgres:latest",
|
||||
);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("api");
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
});
|
||||
@@ -102,49 +102,49 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with depends_on (object) in compose file", () => {
|
||||
test("Add prefix to service names with depends_on (object) in compose file", () => {
|
||||
const composeData = load(composeFile5) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que los nombres en depends_on tienen el prefijo
|
||||
const webDependsOn = actualComposeData.services?.[`web-${suffix}`]
|
||||
?.depends_on as Record<string, any>;
|
||||
expect(webDependsOn).toHaveProperty(`db-${suffix}`);
|
||||
expect(webDependsOn).toHaveProperty(`api-${suffix}`);
|
||||
expect(webDependsOn[`db-${suffix}`].condition).toBe("service_healthy");
|
||||
expect(webDependsOn[`api-${suffix}`].condition).toBe("service_started");
|
||||
const webDependsOn = actualComposeData.services[`web-${prefix}`]
|
||||
.depends_on as Record<string, any>;
|
||||
expect(webDependsOn).toHaveProperty(`db-${prefix}`);
|
||||
expect(webDependsOn).toHaveProperty(`api-${prefix}`);
|
||||
expect(webDependsOn[`db-${prefix}`].condition).toBe("service_healthy");
|
||||
expect(webDependsOn[`api-${prefix}`].condition).toBe("service_started");
|
||||
|
||||
// Verificar que los servicios `db` y `api` también tienen el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("db");
|
||||
expect(actualComposeData.services?.[`db-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`db-${prefix}`].image).toBe(
|
||||
"postgres:latest",
|
||||
);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("api");
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -30,41 +30,41 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with extends (string) in compose file", () => {
|
||||
test("Add prefix to service names with extends (string) in compose file", () => {
|
||||
const composeData = load(composeFile6) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que el nombre en extends tiene el prefijo
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.extends).toBe(
|
||||
`base_service-${suffix}`,
|
||||
expect(actualComposeData.services[`web-${prefix}`].extends).toBe(
|
||||
`base_service-${prefix}`,
|
||||
);
|
||||
|
||||
// Verificar que el servicio `base_service` también tiene el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`base_service-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`base_service-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("base_service");
|
||||
expect(actualComposeData.services?.[`base_service-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`base_service-${prefix}`].image).toBe(
|
||||
"base:latest",
|
||||
);
|
||||
});
|
||||
@@ -90,42 +90,42 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with extends (object) in compose file", () => {
|
||||
test("Add prefix to service names with extends (object) in compose file", () => {
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que el nombre en extends.service tiene el prefijo
|
||||
const webExtends = actualComposeData.services?.[`web-${suffix}`]?.extends;
|
||||
const webExtends = actualComposeData.services[`web-${prefix}`].extends;
|
||||
if (typeof webExtends !== "string") {
|
||||
expect(webExtends?.service).toBe(`base_service-${suffix}`);
|
||||
expect(webExtends.service).toBe(`base_service-${prefix}`);
|
||||
}
|
||||
|
||||
// Verificar que el servicio `base_service` también tiene el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`base_service-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`base_service-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("base_service");
|
||||
expect(actualComposeData.services?.[`base_service-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`base_service-${prefix}`].image).toBe(
|
||||
"base:latest",
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -31,46 +31,46 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with links in compose file", () => {
|
||||
test("Add prefix to service names with links in compose file", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que los nombres en links tienen el prefijo
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.links).toContain(
|
||||
`db-${suffix}`,
|
||||
expect(actualComposeData.services[`web-${prefix}`].links).toContain(
|
||||
`db-${prefix}`,
|
||||
);
|
||||
|
||||
// Verificar que los servicios `db` y `api` también tienen el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("db");
|
||||
expect(actualComposeData.services?.[`db-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`db-${prefix}`].image).toBe(
|
||||
"postgres:latest",
|
||||
);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("api");
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -26,23 +26,23 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to service names in compose file", () => {
|
||||
test("Add prefix to service names in compose file", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que los nombres de los servicios han cambiado correctamente
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
|
||||
// Verificar que las claves originales no existen
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
expect(actualComposeData.services).not.toHaveProperty("api");
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
addSuffixToAllServiceNames,
|
||||
addSuffixToServiceNames,
|
||||
} from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
addPrefixToAllServiceNames,
|
||||
addPrefixToServiceNames,
|
||||
} from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -70,17 +70,17 @@ networks:
|
||||
driver: bridge
|
||||
`);
|
||||
|
||||
test("Add suffix to all service names in compose file", () => {
|
||||
test("Add prefix to all service names in compose file", () => {
|
||||
const composeData = load(composeFileCombinedAllCases) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
@@ -175,11 +175,11 @@ networks:
|
||||
driver: bridge
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all service names in compose file 1", () => {
|
||||
test("Add prefix to all service names in compose file 1", () => {
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile1);
|
||||
});
|
||||
@@ -270,11 +270,11 @@ networks:
|
||||
driver: bridge
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all service names in compose file 2", () => {
|
||||
test("Add prefix to all service names in compose file 2", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile2);
|
||||
});
|
||||
@@ -365,11 +365,11 @@ networks:
|
||||
driver: bridge
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all service names in compose file 3", () => {
|
||||
test("Add prefix to all service names in compose file 3", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedComposeFile3);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -35,44 +35,42 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with volumes_from in compose file", () => {
|
||||
test("Add prefix to service names with volumes_from in compose file", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
const updatedComposeData = addSuffixToServiceNames(
|
||||
const updatedComposeData = addPrefixToServiceNames(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("web");
|
||||
|
||||
// Verificar que la configuración de la imagen sigue igual
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`web-${prefix}`].image).toBe(
|
||||
"nginx:latest",
|
||||
);
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
|
||||
expect(actualComposeData.services[`api-${prefix}`].image).toBe(
|
||||
"myapi:latest",
|
||||
);
|
||||
|
||||
// Verificar que los nombres en volumes_from tienen el prefijo
|
||||
expect(actualComposeData.services?.[`web-${suffix}`]?.volumes_from).toContain(
|
||||
`shared-${suffix}`,
|
||||
expect(actualComposeData.services[`web-${prefix}`].volumes_from).toContain(
|
||||
`shared-${prefix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.[`api-${suffix}`]?.volumes_from).toContain(
|
||||
`shared-${suffix}`,
|
||||
expect(actualComposeData.services[`api-${prefix}`].volumes_from).toContain(
|
||||
`shared-${prefix}`,
|
||||
);
|
||||
|
||||
// Verificar que el servicio shared también tiene el prefijo
|
||||
expect(actualComposeData.services).toHaveProperty(`shared-${suffix}`);
|
||||
expect(actualComposeData.services).toHaveProperty(`shared-${prefix}`);
|
||||
expect(actualComposeData.services).not.toHaveProperty("shared");
|
||||
expect(actualComposeData.services?.[`shared-${suffix}`]?.image).toBe(
|
||||
"busybox",
|
||||
);
|
||||
expect(actualComposeData.services[`shared-${prefix}`].image).toBe("busybox");
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addPrefixToVolumesRoot,
|
||||
addPrefixToAllVolumes,
|
||||
} from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -139,15 +142,15 @@ test("Generate random hash with 8 characters", () => {
|
||||
|
||||
// Docker compose needs unique names for services, volumes, networks and containers
|
||||
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
|
||||
test("Add suffix to volumes root property", () => {
|
||||
test("Add prefix to volumes root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
|
||||
// {
|
||||
// 'db-data-af045046': { driver: 'local' },
|
||||
@@ -157,15 +160,15 @@ test("Add suffix to volumes root property", () => {
|
||||
|
||||
expect(volumes).toBeDefined();
|
||||
for (const volumeKey of Object.keys(volumes)) {
|
||||
expect(volumeKey).toContain(`-${suffix}`);
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("Expect to change the suffix in all the possible places", () => {
|
||||
test("Expect to change the prefix in all the possible places", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerCompose);
|
||||
});
|
||||
@@ -214,11 +217,11 @@ volumes:
|
||||
mongo-data-testhash:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Expect to change the suffix in all the possible places (2 Try)", () => {
|
||||
test("Expect to change the prefix in all the possible places (2 Try)", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerCompose2);
|
||||
});
|
||||
@@ -267,11 +270,11 @@ volumes:
|
||||
mongo-data-testhash:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Expect to change the suffix in all the possible places (3 Try)", () => {
|
||||
test("Expect to change the prefix in all the possible places (3 Try)", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerCompose3);
|
||||
});
|
||||
@@ -1008,11 +1011,11 @@ volumes:
|
||||
db-config-testhash:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
||||
test("Expect to change the prefix in all the possible places (4 Try)", () => {
|
||||
const composeData = load(composeFileComplex) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerComposeComplex);
|
||||
});
|
||||
@@ -1107,11 +1110,11 @@ volumes:
|
||||
db-data-testhash:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
||||
test("Expect to change the prefix in all the possible places (5 Try)", () => {
|
||||
const composeData = load(composeFileExample1) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
|
||||
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToVolumesRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToVolumesRoot } from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -29,18 +29,18 @@ test("Generate random hash with 8 characters", () => {
|
||||
expect(hash.length).toBe(8);
|
||||
});
|
||||
|
||||
test("Add suffix to volumes in root property", () => {
|
||||
test("Add prefix to volumes in root property", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
expect(volumes).toBeDefined();
|
||||
for (const volumeKey of Object.keys(volumes)) {
|
||||
expect(volumeKey).toContain(`-${suffix}`);
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
expect(volumes[volumeKey]).toBeDefined();
|
||||
}
|
||||
});
|
||||
@@ -67,18 +67,18 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to volumes in root property (Case 2)", () => {
|
||||
test("Add prefix to volumes in root property (Case 2)", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
expect(volumes).toBeDefined();
|
||||
for (const volumeKey of Object.keys(volumes)) {
|
||||
expect(volumeKey).toContain(`-${suffix}`);
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
expect(volumes[volumeKey]).toBeDefined();
|
||||
}
|
||||
});
|
||||
@@ -101,19 +101,19 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to volumes in root property (Case 3)", () => {
|
||||
test("Add prefix to volumes in root property (Case 3)", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
|
||||
expect(volumes).toBeDefined();
|
||||
for (const volumeKey of Object.keys(volumes)) {
|
||||
expect(volumeKey).toContain(`-${suffix}`);
|
||||
expect(volumeKey).toContain(`-${prefix}`);
|
||||
expect(volumes[volumeKey]).toBeDefined();
|
||||
}
|
||||
});
|
||||
@@ -179,15 +179,15 @@ volumes:
|
||||
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to volumes in root property", () => {
|
||||
test("Add prefix to volumes in root property", () => {
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
if (!composeData?.volumes) {
|
||||
return;
|
||||
}
|
||||
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
|
||||
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
|
||||
const updatedComposeData = { ...composeData, volumes };
|
||||
|
||||
// Verificar que el resultado coincide con el archivo esperado
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToVolumesInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addPrefixToVolumesInServices } from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -21,22 +21,22 @@ services:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
`;
|
||||
|
||||
test("Add suffix to volumes declared directly in services", () => {
|
||||
test("Add prefix to volumes declared directly in services", () => {
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addSuffixToVolumesInServices(
|
||||
const updatedComposeData = addPrefixToVolumesInServices(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
expect(actualComposeData.services?.db?.volumes).toContain(
|
||||
`db_data-${suffix}:/var/lib/postgresql/data`,
|
||||
`db_data-${prefix}:/var/lib/postgresql/data`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -56,25 +56,25 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
test("Add suffix to volumes declared directly in services (Case 2)", () => {
|
||||
test("Add prefix to volumes declared directly in services (Case 2)", () => {
|
||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
const prefix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedComposeData = addSuffixToVolumesInServices(
|
||||
const updatedComposeData = addPrefixToVolumesInServices(
|
||||
composeData.services,
|
||||
suffix,
|
||||
prefix,
|
||||
);
|
||||
const actualComposeData = { ...composeData, services: updatedComposeData };
|
||||
|
||||
expect(actualComposeData.services?.db?.volumes).toEqual([
|
||||
{
|
||||
type: "volume",
|
||||
source: `db-test-${suffix}`,
|
||||
source: `db-test-${prefix}`,
|
||||
target: "/var/lib/postgresql/data",
|
||||
},
|
||||
]);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addSuffixToAllVolumes,
|
||||
addSuffixToVolumesInServices,
|
||||
} from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
addPrefixToAllVolumes,
|
||||
addPrefixToVolumesInServices,
|
||||
} from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
@@ -47,12 +47,12 @@ volumes:
|
||||
driver: local
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to volumes with type: volume in services", () => {
|
||||
test("Add prefix to volumes with type: volume in services", () => {
|
||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
const actualComposeData = { ...composeData, ...updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume);
|
||||
@@ -96,12 +96,12 @@ volumes:
|
||||
driver: local
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to mixed volumes in services", () => {
|
||||
test("Add prefix to mixed volumes in services", () => {
|
||||
const composeData = load(composeFileTypeVolume1) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
const actualComposeData = { ...composeData, ...updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume1);
|
||||
@@ -157,12 +157,12 @@ volumes:
|
||||
device: /path/to/app/logs
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to complex volume configurations in services", () => {
|
||||
test("Add prefix to complex volume configurations in services", () => {
|
||||
const composeData = load(composeFileTypeVolume2) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
const actualComposeData = { ...composeData, ...updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume2);
|
||||
@@ -276,12 +276,12 @@ volumes:
|
||||
device: /path/to/shared/logs
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to complex nested volumes configuration in services", () => {
|
||||
test("Add prefix to complex nested volumes configuration in services", () => {
|
||||
const composeData = load(composeFileTypeVolume3) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const prefix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
|
||||
const actualComposeData = { ...composeData, ...updatedComposeData };
|
||||
|
||||
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume3);
|
||||
@@ -1,24 +1,16 @@
|
||||
import path from "node:path";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
root: "./",
|
||||
projects: ["tsconfig.json"],
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||
pool: "forks",
|
||||
},
|
||||
define: {
|
||||
"process.env": {
|
||||
NODE: "test",
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@dokploy/server": path.resolve(
|
||||
__dirname,
|
||||
"../../../packages/server/src",
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
LEMON_SQUEEZY_API_KEY=""
|
||||
LEMON_SQUEEZY_STORE_ID=""
|
||||
28
apps/api/.gitignore
vendored
28
apps/api/.gitignore
vendored
@@ -1,28 +0,0 @@
|
||||
# dev
|
||||
.yarn/
|
||||
!.yarn/releases
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea/workspace.xml
|
||||
.idea/usage.statistics.xml
|
||||
.idea/shelf
|
||||
|
||||
# deps
|
||||
node_modules/
|
||||
|
||||
# env
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -1,8 +0,0 @@
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```
|
||||
open http://localhost:3000
|
||||
```
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "@dokploy/api",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"zod": "^3.23.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"hono": "^4.5.8",
|
||||
"dotenv": "^16.3.1",
|
||||
"redis": "4.7.0",
|
||||
"@nerimity/mimiqueue": "1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.2",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1"
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0"
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
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";
|
||||
|
||||
const app = new Hono();
|
||||
const redisClient = createClient({
|
||||
url: process.env.REDIS_URL,
|
||||
});
|
||||
|
||||
app.use(async (c, next) => {
|
||||
if (c.req.path === "/health") {
|
||||
return next();
|
||||
}
|
||||
const authHeader = c.req.header("X-API-Key");
|
||||
|
||||
if (process.env.API_KEY !== authHeader) {
|
||||
return c.json({ message: "Invalid API Key" }, 403);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||
const data = c.req.valid("json");
|
||||
const res = queue.add(data, { groupName: data.serverId });
|
||||
return c.json(
|
||||
{
|
||||
message: "Deployment Added",
|
||||
},
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/health", async (c) => {
|
||||
return c.json({ status: "ok" });
|
||||
});
|
||||
|
||||
const queue = new Queue({
|
||||
name: "deployments",
|
||||
process: async (job: DeployJob) => {
|
||||
logger.info("Deploying job", job);
|
||||
return await deploy(job);
|
||||
},
|
||||
redisClient,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await redisClient.connect();
|
||||
await redisClient.flushAll();
|
||||
logger.info("Redis Cleaned");
|
||||
})();
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || "3000");
|
||||
logger.info("Starting Deployments Server ✅", port);
|
||||
serve({ fetch: app.fetch, port });
|
||||
@@ -1,10 +0,0 @@
|
||||
import pino from "pino";
|
||||
|
||||
export const logger = pino({
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application"),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("compose"),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
previewDeploymentId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy"]),
|
||||
applicationType: z.literal("application-preview"),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||
@@ -1,82 +0,0 @@
|
||||
import {
|
||||
deployRemoteApplication,
|
||||
deployRemoteCompose,
|
||||
deployRemotePreviewApplication,
|
||||
rebuildRemoteApplication,
|
||||
rebuildRemoteCompose,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updatePreviewDeployment,
|
||||
} from "@dokploy/server";
|
||||
import type { DeployJob } from "./schema";
|
||||
|
||||
export const deploy = async (job: DeployJob) => {
|
||||
try {
|
||||
if (job.applicationType === "application") {
|
||||
await updateApplicationStatus(job.applicationId, "running");
|
||||
if (job.server) {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (job.applicationType === "compose") {
|
||||
await updateCompose(job.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
|
||||
if (job.server) {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (job.applicationType === "application-preview") {
|
||||
await updatePreviewDeployment(job.previewDeploymentId, {
|
||||
previewStatus: "running",
|
||||
});
|
||||
if (job.server) {
|
||||
if (job.type === "deploy") {
|
||||
await deployRemotePreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
previewDeploymentId: job.previewDeploymentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (job.applicationType === "application") {
|
||||
await updateApplicationStatus(job.applicationId, "error");
|
||||
} else if (job.applicationType === "compose") {
|
||||
await updateCompose(job.composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
} else if (job.applicationType === "application-preview") {
|
||||
await updatePreviewDeployment(job.previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
dist
|
||||
59
apps/dokploy/.gitignore
vendored
59
apps/dokploy/.gitignore
vendored
@@ -1,59 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
/redis-data
|
||||
traefik.yml
|
||||
.docker
|
||||
.env.production
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/dist
|
||||
/production-server
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
/logs
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
/dokploy
|
||||
/config
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# otros
|
||||
/.data
|
||||
/.main
|
||||
.vscode
|
||||
|
||||
*.lockb
|
||||
*.rdb
|
||||
.idea
|
||||
@@ -1 +0,0 @@
|
||||
18.18.0
|
||||
@@ -1,242 +0,0 @@
|
||||
|
||||
|
||||
# Contributing
|
||||
|
||||
Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute.
|
||||
|
||||
|
||||
Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues.
|
||||
|
||||
We have a few guidelines to follow when contributing to this project:
|
||||
|
||||
- [Commit Convention](#commit-convention)
|
||||
- [Setup](#setup)
|
||||
- [Development](#development)
|
||||
- [Build](#build)
|
||||
- [Pull Request](#pull-request)
|
||||
|
||||
## Commit Convention
|
||||
|
||||
Before you craete a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
### Commit Message Format
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
#### Type
|
||||
Must be one of the following:
|
||||
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **docs**: Documentation only changes
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **perf**: A code change that improves performance
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
|
||||
* **chore**: Other changes that don't modify `src` or `test` files
|
||||
* **revert**: Reverts a previous commit
|
||||
|
||||
Example:
|
||||
```
|
||||
feat: add new feature
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dokploy/dokploy.git
|
||||
cd dokploy
|
||||
pnpm install
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Is required to have **Docker** installed on your machine.
|
||||
|
||||
|
||||
### Setup
|
||||
|
||||
Run the command that will spin up all the required services and files.
|
||||
|
||||
```bash
|
||||
pnpm run setup
|
||||
```
|
||||
|
||||
Now run the development server.
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
|
||||
Go to http://localhost:3000 to see the development server
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
To build the docker image
|
||||
```bash
|
||||
pnpm run docker:build
|
||||
```
|
||||
|
||||
To push the docker image
|
||||
```bash
|
||||
pnpm run docker:push
|
||||
```
|
||||
|
||||
## Password Reset
|
||||
|
||||
In the case you lost your password, you can reset it using the following command
|
||||
|
||||
```bash
|
||||
pnpm run reset-password
|
||||
```
|
||||
|
||||
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
|
||||
|
||||
```bash
|
||||
bunx lt --port 3000
|
||||
```
|
||||
|
||||
If you run into permission issues of docker run the following command
|
||||
|
||||
```bash
|
||||
sudo chown -R USERNAME dokploy or sudo chown -R $(whoami) ~/.docker
|
||||
```
|
||||
|
||||
## Application deploy
|
||||
|
||||
In case you want to deploy the application on your machine and you selected nixpacks or buildpacks, you need to install first.
|
||||
|
||||
```bash
|
||||
# Install Nixpacks
|
||||
curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& chmod +x install.sh \
|
||||
&& ./install.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
# Install Buildpacks
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
```
|
||||
|
||||
|
||||
## Pull Request
|
||||
|
||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||
- Create a new branch for each feature or bug fix.
|
||||
- Make sure to add tests for your changes.
|
||||
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
|
||||
- When creating a pull request, please provide a clear and concise description of the changes made.
|
||||
- If you include a video or screenshot, would be awesome so we can see the changes in action.
|
||||
- If your pull request fixes an open issue, please reference the issue in the pull request description.
|
||||
- Once your pull request is merged, you will be automatically added as a contributor to the project.
|
||||
|
||||
Thank you for your contribution!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Templates
|
||||
|
||||
To add a new template, go to `templates` folder and create a new folder with the name of the template.
|
||||
|
||||
Let's take the example of `plausible` template.
|
||||
|
||||
1. create a folder in `templates/plausible`
|
||||
2. create a `docker-compose.yml` file inside the folder with the content of compose.
|
||||
3. create a `index.ts` file inside the folder with the following code as base:
|
||||
4. When creating a pull request, please provide a video of the template working in action.
|
||||
|
||||
```typescript
|
||||
// EXAMPLE
|
||||
import {
|
||||
generateHash,
|
||||
generateRandomDomain,
|
||||
type Template,
|
||||
type Schema,
|
||||
} from "../utils";
|
||||
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
|
||||
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
const secretBase = generateBase64(64);
|
||||
const toptKeyBase = generateBase64(32);
|
||||
|
||||
const envs = [
|
||||
// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
||||
`PLAUSIBLE_HOST=${randomDomain}`,
|
||||
"PLAUSIBLE_PORT=8000",
|
||||
`BASE_URL=http://${randomDomain}`,
|
||||
`SECRET_KEY_BASE=${secretBase}`,
|
||||
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
];
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
||||
content: `some content......`,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
mounts,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties:
|
||||
|
||||
**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.**
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: "plausible",
|
||||
name: "Plausible",
|
||||
version: "v2.1.0",
|
||||
description:
|
||||
"Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.",
|
||||
logo: "plausible.svg", // we defined the name and the extension of the logo
|
||||
links: {
|
||||
github: "https://github.com/plausible/plausible",
|
||||
website: "https://plausible.io/",
|
||||
docs: "https://plausible.io/docs",
|
||||
},
|
||||
tags: ["analytics"],
|
||||
load: () => import("./plausible/index").then((m) => m.generate),
|
||||
},
|
||||
```
|
||||
|
||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
||||
|
||||
|
||||
### Recomendations
|
||||
- Use the same name of the folder as the id of the template.
|
||||
- The logo should be in the public folder.
|
||||
- If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
||||
- Test first on a vps or a server to make sure the template works.
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
|
||||
# Build only the dokploy app
|
||||
RUN pnpm run dokploy:build
|
||||
|
||||
# Deploy only the dokploy app
|
||||
RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
|
||||
|
||||
FROM base AS dokploy
|
||||
COPY --from=build /prod/dokploy /prod/dokploy
|
||||
WORKDIR /prod/dokploy
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
@@ -1,26 +0,0 @@
|
||||
# License
|
||||
|
||||
## Core License (Apache License 2.0)
|
||||
|
||||
Copyright 2024 Mauricio Siu.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
## Additional Terms for Specific Features
|
||||
|
||||
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||
|
||||
For further inquiries or permissions, please contact us directly.
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import { createDomainLabels } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("createDomainLabels", () => {
|
||||
const appName = "test-app";
|
||||
const baseDomain: Domain = {
|
||||
host: "example.com",
|
||||
port: 8080,
|
||||
https: false,
|
||||
uniqueConfigKey: 1,
|
||||
certificateType: "none",
|
||||
applicationId: "",
|
||||
composeId: "",
|
||||
domainType: "compose",
|
||||
serviceName: "test-app",
|
||||
domainId: "",
|
||||
path: "/",
|
||||
createdAt: "",
|
||||
previewDeploymentId: "",
|
||||
};
|
||||
|
||||
it("should create basic labels for web entrypoint", async () => {
|
||||
const labels = await createDomainLabels(appName, baseDomain, "web");
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-web.rule=Host(`example.com`)",
|
||||
"traefik.http.routers.test-app-1-web.entrypoints=web",
|
||||
"traefik.http.services.test-app-1-web.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-web.service=test-app-1-web",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create labels for websecure entrypoint", async () => {
|
||||
const labels = await createDomainLabels(appName, baseDomain, "websecure");
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`)",
|
||||
"traefik.http.routers.test-app-1-websecure.entrypoints=websecure",
|
||||
"traefik.http.services.test-app-1-websecure.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-websecure.service=test-app-1-websecure",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should add the path prefix if is different than / empty", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
path: "/hello",
|
||||
},
|
||||
"websecure",
|
||||
);
|
||||
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`) && PathPrefix(`/hello`)",
|
||||
"traefik.http.routers.test-app-1-websecure.entrypoints=websecure",
|
||||
"traefik.http.services.test-app-1-websecure.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-websecure.service=test-app-1-websecure",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should add redirect middleware for https on web entrypoint", async () => {
|
||||
const httpsBaseDomain = { ...baseDomain, https: true };
|
||||
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file",
|
||||
);
|
||||
});
|
||||
|
||||
it("should add Let's Encrypt configuration for websecure with letsencrypt certificate", async () => {
|
||||
const letsencryptDomain = {
|
||||
...baseDomain,
|
||||
https: true,
|
||||
certificateType: "letsencrypt" as const,
|
||||
};
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
letsencryptDomain,
|
||||
"websecure",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not add Let's Encrypt configuration for non-letsencrypt certificate", async () => {
|
||||
const nonLetsencryptDomain = {
|
||||
...baseDomain,
|
||||
https: true,
|
||||
certificateType: "none" as const,
|
||||
};
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
nonLetsencryptDomain,
|
||||
"websecure",
|
||||
);
|
||||
expect(labels).not.toContain(
|
||||
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle different ports correctly", async () => {
|
||||
const customPortDomain = { ...baseDomain, port: 3000 };
|
||||
const labels = await createDomainLabels(appName, customPortDomain, "web");
|
||||
expect(labels).toContain(
|
||||
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { addDokployNetworkToRoot } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("addDokployNetworkToRoot", () => {
|
||||
it("should create network object if networks is undefined", () => {
|
||||
const result = addDokployNetworkToRoot(undefined);
|
||||
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||
});
|
||||
|
||||
it("should add network to an empty object", () => {
|
||||
const result = addDokployNetworkToRoot({});
|
||||
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||
});
|
||||
|
||||
it("should not modify existing network configuration", () => {
|
||||
const existing = { "dokploy-network": { external: false } };
|
||||
const result = addDokployNetworkToRoot(existing);
|
||||
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||
});
|
||||
|
||||
it("should add network alongside existing networks", () => {
|
||||
const existing = { "other-network": { external: true } };
|
||||
const result = addDokployNetworkToRoot(existing);
|
||||
expect(result).toEqual({
|
||||
"other-network": { external: true },
|
||||
"dokploy-network": { external: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { addDokployNetworkToService } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("addDokployNetworkToService", () => {
|
||||
it("should add network to an empty array", () => {
|
||||
const result = addDokployNetworkToService([]);
|
||||
expect(result).toEqual(["dokploy-network"]);
|
||||
});
|
||||
|
||||
it("should not add duplicate network to an array", () => {
|
||||
const result = addDokployNetworkToService(["dokploy-network"]);
|
||||
expect(result).toEqual(["dokploy-network"]);
|
||||
});
|
||||
|
||||
it("should add network to an existing array with other networks", () => {
|
||||
const result = addDokployNetworkToService(["other-network"]);
|
||||
expect(result).toEqual(["other-network", "dokploy-network"]);
|
||||
});
|
||||
|
||||
it("should add network to an object if networks is an object", () => {
|
||||
const result = addDokployNetworkToService({ "other-network": {} });
|
||||
expect(result).toEqual({ "other-network": {}, "dokploy-network": {} });
|
||||
});
|
||||
});
|
||||
@@ -1,273 +0,0 @@
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNetworks } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
- backend
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services", () => {
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData?.services?.web?.networks).toContain(
|
||||
`frontend-${suffix}`,
|
||||
);
|
||||
|
||||
expect(actualComposeData?.services?.api?.networks).toContain(
|
||||
`backend-${suffix}`,
|
||||
);
|
||||
|
||||
const apiNetworks = actualComposeData?.services?.api?.networks;
|
||||
|
||||
expect(apiNetworks).toBeDefined();
|
||||
expect(actualComposeData?.services?.api?.networks).toContain(
|
||||
`backend-${suffix}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Caso 2: Objeto con aliases
|
||||
const composeFile2 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- api
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services with aliases", () => {
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData.services?.api?.networks).toHaveProperty(
|
||||
`frontend-${suffix}`,
|
||||
);
|
||||
|
||||
const networkConfig = actualComposeData?.services?.api?.networks as {
|
||||
[key: string]: { aliases?: string[] };
|
||||
};
|
||||
expect(networkConfig[`frontend-${suffix}`]).toBeDefined();
|
||||
expect(networkConfig[`frontend-${suffix}`]?.aliases).toContain("api");
|
||||
|
||||
expect(actualComposeData.services?.api?.networks).not.toHaveProperty(
|
||||
"frontend-ash",
|
||||
);
|
||||
});
|
||||
|
||||
const composeFile3 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
backend:
|
||||
|
||||
networks:
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services (Object with simple networks)", () => {
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
expect(actualComposeData.services?.redis?.networks).toHaveProperty(
|
||||
`backend-${suffix}`,
|
||||
);
|
||||
});
|
||||
|
||||
const composeFileCombined = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- api
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
backend:
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
driver: bridge
|
||||
|
||||
backend:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services (combined case)", () => {
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||
const actualComposeData = { ...composeData, services };
|
||||
|
||||
// Caso 1: ListOfStrings
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`frontend-${suffix}`,
|
||||
);
|
||||
expect(actualComposeData.services?.web?.networks).toContain(
|
||||
`backend-${suffix}`,
|
||||
);
|
||||
|
||||
// Caso 2: Objeto con aliases
|
||||
const apiNetworks = actualComposeData.services?.api?.networks as {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
expect(apiNetworks).toHaveProperty(`frontend-${suffix}`);
|
||||
expect(apiNetworks[`frontend-${suffix}`]).toBeDefined();
|
||||
expect(apiNetworks).not.toHaveProperty("frontend");
|
||||
|
||||
// Caso 3: Objeto con redes simples
|
||||
const redisNetworks = actualComposeData.services?.redis?.networks;
|
||||
expect(redisNetworks).toHaveProperty(`backend-${suffix}`);
|
||||
expect(redisNetworks).not.toHaveProperty("backend");
|
||||
});
|
||||
|
||||
const composeFile7 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- dokploy-network
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network in services", () => {
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||
const service = networks.web;
|
||||
|
||||
expect(service).toBeDefined();
|
||||
expect(service?.networks).toContain("dokploy-network");
|
||||
});
|
||||
|
||||
const composeFile8 = `
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
- dokploy-network
|
||||
|
||||
|
||||
api:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
frontend:
|
||||
aliases:
|
||||
- api
|
||||
dokploy-network:
|
||||
aliases:
|
||||
- api
|
||||
redis:
|
||||
image: redis:alpine
|
||||
networks:
|
||||
dokploy-network:
|
||||
db:
|
||||
image: myapi:latest
|
||||
networks:
|
||||
dokploy-network:
|
||||
aliases:
|
||||
- apid
|
||||
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
|
||||
const composeData = load(composeFile8) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.services) {
|
||||
return;
|
||||
}
|
||||
const networks = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||
const service = networks.web;
|
||||
const api = networks.api;
|
||||
const redis = networks.redis;
|
||||
const db = networks.db;
|
||||
|
||||
const dbNetworks = db?.networks as {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const apiNetworks = api?.networks as {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
expect(service).toBeDefined();
|
||||
expect(service?.networks).toContain("dokploy-network");
|
||||
|
||||
expect(redis?.networks).toHaveProperty("dokploy-network");
|
||||
expect(dbNetworks["dokploy-network"]).toBeDefined();
|
||||
expect(apiNetworks["dokploy-network"]).toBeDefined();
|
||||
});
|
||||
@@ -1,205 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
const { APPLICATIONS_PATH } = paths();
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { unzipDrop } from "@dokploy/server";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
// @ts-ignore
|
||||
...actual,
|
||||
paths: () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
const undici = require("undici");
|
||||
globalThis.File = undici.File as any;
|
||||
globalThis.FileList = undici.FileList as any;
|
||||
}
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
applicationId: "",
|
||||
herokuVersion: "",
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
serverId: "",
|
||||
registryUrl: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
isPreviewDeploymentsActive: false,
|
||||
previewBuildArgs: null,
|
||||
previewCertificateType: "none",
|
||||
previewEnv: null,
|
||||
previewHttps: false,
|
||||
previewPath: "/",
|
||||
previewPort: 3000,
|
||||
previewLimit: 0,
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
env: "",
|
||||
adminId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
projectId: "",
|
||||
},
|
||||
buildArgs: null,
|
||||
buildPath: "/",
|
||||
gitlabPathNamespace: "",
|
||||
buildType: "nixpacks",
|
||||
bitbucketBranch: "",
|
||||
bitbucketBuildPath: "",
|
||||
bitbucketId: "",
|
||||
bitbucketRepository: "",
|
||||
bitbucketOwner: "",
|
||||
githubId: "",
|
||||
gitlabProjectId: 0,
|
||||
gitlabBranch: "",
|
||||
gitlabBuildPath: "",
|
||||
gitlabId: "",
|
||||
gitlabRepository: "",
|
||||
gitlabOwner: "",
|
||||
command: null,
|
||||
cpuLimit: null,
|
||||
cpuReservation: null,
|
||||
createdAt: "",
|
||||
customGitBranch: "",
|
||||
customGitBuildPath: "",
|
||||
customGitSSHKeyId: null,
|
||||
customGitUrl: "",
|
||||
description: "",
|
||||
dockerfile: null,
|
||||
dockerImage: null,
|
||||
dropBuildPath: null,
|
||||
enabled: null,
|
||||
env: null,
|
||||
healthCheckSwarm: null,
|
||||
labelsSwarm: null,
|
||||
memoryLimit: null,
|
||||
memoryReservation: null,
|
||||
modeSwarm: null,
|
||||
mounts: [],
|
||||
name: "",
|
||||
networkSwarm: null,
|
||||
owner: null,
|
||||
password: null,
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
redirects: [],
|
||||
refreshToken: "",
|
||||
registry: null,
|
||||
registryId: null,
|
||||
replicas: 1,
|
||||
repository: null,
|
||||
restartPolicySwarm: null,
|
||||
rollbackConfigSwarm: null,
|
||||
security: [],
|
||||
sourceType: "git",
|
||||
subtitle: null,
|
||||
title: null,
|
||||
updateConfigSwarm: null,
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
};
|
||||
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
// const { APPLICATIONS_PATH } = paths();
|
||||
beforeAll(async () => {
|
||||
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root folder", async () => {
|
||||
baseApp.appName = "single-file";
|
||||
// const appName = "single-file";
|
||||
try {
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||
console.log(`Output Path: ${outputPath}`);
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||
baseApp.appName = "folderwithfile";
|
||||
// const appName = "folderwithfile";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with multiple root folders", async () => {
|
||||
baseApp.appName = "two-folders";
|
||||
// const appName = "two-folders";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root with a file", async () => {
|
||||
baseApp.appName = "nested";
|
||||
// const appName = "nested";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||
expect(files.some((f) => f.name === "folder3")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||
baseApp.appName = "folder-with-sibling-file";
|
||||
// const appName = "folder-with-sibling-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
});
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
Gogogogogogo
|
||||
@@ -1 +0,0 @@
|
||||
gogogogogog
|
||||
@@ -1 +0,0 @@
|
||||
gogogogogogogogogo
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
dsafasdfasdf
|
||||
Binary file not shown.
179
apps/dokploy/__test__/env/shared.test.ts
vendored
179
apps/dokploy/__test__/env/shared.test.ts
vendored
@@ -1,179 +0,0 @@
|
||||
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=staging
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||
PORT=3000
|
||||
`;
|
||||
const serviceEnv = `
|
||||
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||
SERVICE_PORT=4000
|
||||
`;
|
||||
|
||||
describe("prepareEnvironmentVariables", () => {
|
||||
it("resolves project variables correctly", () => {
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"ENVIRONMENT=staging",
|
||||
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||
"SERVICE_PORT=4000",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles undefined project variables", () => {
|
||||
const incompleteProjectEnv = `
|
||||
NODE_ENV=production
|
||||
`;
|
||||
|
||||
const invalidServiceEnv = `
|
||||
UNDEFINED_VAR=\${{project.UNDEFINED_VAR}}
|
||||
`;
|
||||
|
||||
expect(
|
||||
() =>
|
||||
prepareEnvironmentVariables(invalidServiceEnv, incompleteProjectEnv), // Cambiado el orden
|
||||
).toThrow("Invalid project environment variable: project.UNDEFINED_VAR");
|
||||
});
|
||||
it("allows service-specific variables to override project variables", () => {
|
||||
const serviceSpecificEnv = `
|
||||
ENVIRONMENT=production
|
||||
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(
|
||||
serviceSpecificEnv,
|
||||
projectEnv,
|
||||
);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"ENVIRONMENT=production", // Overrides project variable
|
||||
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves complex references for dynamic endpoints", () => {
|
||||
const projectEnv = `
|
||||
BASE_URL=https://api.example.com
|
||||
API_VERSION=v1
|
||||
PORT=8000
|
||||
`;
|
||||
const serviceEnv = `
|
||||
API_ENDPOINT=\${{project.BASE_URL}}/\${{project.API_VERSION}}/endpoint
|
||||
SERVICE_PORT=9000
|
||||
`;
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"API_ENDPOINT=https://api.example.com/v1/endpoint",
|
||||
"SERVICE_PORT=9000",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles missing project variables gracefully", () => {
|
||||
const projectEnv = `
|
||||
PORT=8080
|
||||
`;
|
||||
const serviceEnv = `
|
||||
MISSING_VAR=\${{project.MISSING_KEY}}
|
||||
SERVICE_PORT=3000
|
||||
`;
|
||||
|
||||
expect(() => prepareEnvironmentVariables(serviceEnv, projectEnv)).toThrow(
|
||||
"Invalid project environment variable: project.MISSING_KEY",
|
||||
);
|
||||
});
|
||||
|
||||
it("overrides project variables with service-specific values", () => {
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=staging
|
||||
DATABASE_URL=postgres://project:project@localhost:5432/project_db
|
||||
`;
|
||||
const serviceEnv = `
|
||||
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||
DATABASE_URL=postgres://service:service@localhost:5432/service_db
|
||||
SERVICE_NAME=my-service
|
||||
`;
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"ENVIRONMENT=staging",
|
||||
"DATABASE_URL=postgres://service:service@localhost:5432/service_db",
|
||||
"SERVICE_NAME=my-service",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles project variables with normal and unusual characters", () => {
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=PRODUCTION
|
||||
`;
|
||||
|
||||
// Needs to be in quotes
|
||||
const serviceEnv = `
|
||||
NODE_ENV=\${{project.ENVIRONMENT}}
|
||||
SPECIAL_VAR="$^@$^@#$^@!#$@#$-\${{project.ENVIRONMENT}}"
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV=PRODUCTION",
|
||||
"SPECIAL_VAR=$^@$^@#$^@!#$@#$-PRODUCTION",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles complex cases with multiple references, special characters, and spaces", () => {
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=STAGING
|
||||
APP_NAME=MyApp
|
||||
`;
|
||||
|
||||
const serviceEnv = `
|
||||
NODE_ENV=\${{project.ENVIRONMENT}}
|
||||
COMPLEX_VAR="Prefix-$#^!@-\${{project.ENVIRONMENT}}--\${{project.APP_NAME}} Suffix "
|
||||
`;
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV=STAGING",
|
||||
"COMPLEX_VAR=Prefix-$#^!@-STAGING--MyApp Suffix ",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles references enclosed in single quotes", () => {
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=STAGING
|
||||
APP_NAME=MyApp
|
||||
`;
|
||||
|
||||
const serviceEnv = `
|
||||
NODE_ENV='\${{project.ENVIRONMENT}}'
|
||||
COMPLEX_VAR='Prefix-$#^!@-\${{project.ENVIRONMENT}}--\${{project.APP_NAME}} Suffix'
|
||||
`;
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV=STAGING",
|
||||
"COMPLEX_VAR=Prefix-$#^!@-STAGING--MyApp Suffix",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles double and single quotes combined", () => {
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=PRODUCTION
|
||||
APP_NAME=MyApp
|
||||
`;
|
||||
const serviceEnv = `
|
||||
NODE_ENV="'\${{project.ENVIRONMENT}}'"
|
||||
COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'"
|
||||
`;
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv);
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"NODE_ENV='PRODUCTION'",
|
||||
"COMPLEX_VAR='Prefix \"DoubleQuoted\" and MyApp'",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { parseRawConfig, processLogs } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
||||
|
||||
describe("processLogs", () => {
|
||||
it("should process a single log entry correctly", () => {
|
||||
const result = processLogs(sampleLogEntry);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
hour: "2024-08-25T04:00:00Z",
|
||||
count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("should process multiple log entries and group by hour", () => {
|
||||
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:58094","ClientHost":"172.19.0.1","ClientPort":"58094","ClientUsername":"-","DownstreamContentSize":50,"DownstreamStatus":200,"Duration":35914250,"OriginContentSize":50,"OriginDuration":35817959,"OriginStatus":200,"Overhead":96291,"RequestAddr":"s222-pocketbase-f4a6e5.traefik.me","RequestContentSize":0,"RequestCount":991,"RequestHost":"s222-pocketbase-f4a6e5.traefik.me","RequestMethod":"GET","RequestPath":"/api/logs/stats?filter=","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-pocketbase-e94e25-44-web@docker","ServiceAddr":"10.0.1.12:80","ServiceName":"s222-pocketbase-e94e25-44-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.12:80","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T17:44:29.274072471Z","StartUTC":"2024-08-25T17:44:29.274072471Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T17:44:29Z"}
|
||||
{"ClientAddr":"172.19.0.1:58108","ClientHost":"172.19.0.1","ClientPort":"58108","ClientUsername":"-","DownstreamContentSize":30975,"DownstreamStatus":200,"Duration":31406458,"OriginContentSize":30975,"OriginDuration":31046791,"OriginStatus":200,"Overhead":359667,"RequestAddr":"s222-pocketbase-f4a6e5.traefik.me","RequestContentSize":0,"RequestCount":992,"RequestHost":"s222-pocketbase-f4a6e5.traefik.me","RequestMethod":"GET","RequestPath":"/api/logs?page=1\u0026perPage=50\u0026sort=-rowid\u0026skipTotal=1\u0026filter=","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-pocketbase-e94e25-44-web@docker","ServiceAddr":"10.0.1.12:80","ServiceName":"s222-pocketbase-e94e25-44-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.12:80","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T17:44:29.278990221Z","StartUTC":"2024-08-25T17:44:29.278990221Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T17:44:29Z"}
|
||||
`;
|
||||
|
||||
const result = processLogs(sampleLogEntry);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toEqual([{ hour: "2024-08-25T17:00:00Z", count: 2 }]);
|
||||
});
|
||||
|
||||
it("should return an empty array for empty input", () => {
|
||||
expect(processLogs("")).toEqual([]);
|
||||
expect(processLogs(null as any)).toEqual([]);
|
||||
expect(processLogs(undefined as any)).toEqual([]);
|
||||
});
|
||||
|
||||
// it("should parse a single log entry correctly", () => {
|
||||
// const result = parseRawConfig(sampleLogEntry);
|
||||
// expect(result).toHaveLength(1);
|
||||
// expect(result.data[0]).toHaveProperty("ClientAddr", "172.19.0.1:56732");
|
||||
// expect(result.data[0]).toHaveProperty(
|
||||
// "StartUTC",
|
||||
// "2024-08-25T04:34:37.306691884Z",
|
||||
// );
|
||||
// });
|
||||
|
||||
it("should parse multiple log entries", () => {
|
||||
const multipleEntries = `${sampleLogEntry}\n${sampleLogEntry}`;
|
||||
const result = parseRawConfig(multipleEntries);
|
||||
expect(result.data).toHaveLength(2);
|
||||
|
||||
for (const entry of result.data) {
|
||||
expect(entry).toHaveProperty("ClientAddr", "172.19.0.1:56732");
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle whitespace and empty lines", () => {
|
||||
const entryWithWhitespace = `\n${sampleLogEntry}\n\n${sampleLogEntry}\n`;
|
||||
const result = parseRawConfig(entryWithWhitespace);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { fs, vol } from "memfs";
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
...fs,
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { Admin, FileConfig } from "@dokploy/server";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
updateServerTraefik,
|
||||
} from "@dokploy/server";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: Admin = {
|
||||
createdAt: "",
|
||||
authId: "",
|
||||
adminId: "string",
|
||||
serverIp: null,
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
letsEncryptEmail: null,
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
enableLogRotation: false,
|
||||
serversQuantity: 0,
|
||||
stripeCustomerId: "",
|
||||
stripeSubscriptionId: "",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
createDefaultServerTraefikConfig();
|
||||
});
|
||||
|
||||
test("Should read the configuration file", () => {
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe(
|
||||
"dokploy-service-app",
|
||||
);
|
||||
});
|
||||
|
||||
test("Should apply redirect-to-https", () => {
|
||||
updateServerTraefik(
|
||||
{
|
||||
...baseAdmin,
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"example.com",
|
||||
);
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(config.http?.routers?.["dokploy-router-app"]?.middlewares).toContain(
|
||||
"redirect-to-https",
|
||||
);
|
||||
});
|
||||
|
||||
test("Should change only host when no certificate", () => {
|
||||
updateServerTraefik(baseAdmin, "example.com");
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(config.http?.routers?.["dokploy-router-app-secure"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Should not touch config without host", () => {
|
||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
updateServerTraefik(baseAdmin, null);
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(originalConfig).toEqual(config);
|
||||
});
|
||||
|
||||
test("Should remove websecure if https rollback to http", () => {
|
||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
updateServerTraefik(
|
||||
{ ...baseAdmin, certificateType: "letsencrypt" },
|
||||
"example.com",
|
||||
);
|
||||
|
||||
updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com");
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(config.http?.routers?.["dokploy-router-app-secure"]).toBeUndefined();
|
||||
expect(
|
||||
config.http?.routers?.["dokploy-router-app"]?.middlewares,
|
||||
).not.toContain("redirect-to-https");
|
||||
});
|
||||
@@ -1,238 +0,0 @@
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import type { Redirect } from "@dokploy/server";
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { createRouterConfig } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
applicationId: "",
|
||||
herokuVersion: "",
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
serverId: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
registryUrl: "",
|
||||
buildArgs: null,
|
||||
isPreviewDeploymentsActive: false,
|
||||
previewBuildArgs: null,
|
||||
previewCertificateType: "none",
|
||||
previewEnv: null,
|
||||
previewHttps: false,
|
||||
previewPath: "/",
|
||||
previewPort: 3000,
|
||||
previewLimit: 0,
|
||||
previewWildcard: "",
|
||||
project: {
|
||||
env: "",
|
||||
adminId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
createdAt: "",
|
||||
projectId: "",
|
||||
},
|
||||
buildPath: "/",
|
||||
gitlabPathNamespace: "",
|
||||
buildType: "nixpacks",
|
||||
bitbucketBranch: "",
|
||||
bitbucketBuildPath: "",
|
||||
bitbucketId: "",
|
||||
bitbucketRepository: "",
|
||||
bitbucketOwner: "",
|
||||
githubId: "",
|
||||
gitlabProjectId: 0,
|
||||
gitlabBranch: "",
|
||||
gitlabBuildPath: "",
|
||||
gitlabId: "",
|
||||
gitlabRepository: "",
|
||||
gitlabOwner: "",
|
||||
command: null,
|
||||
cpuLimit: null,
|
||||
cpuReservation: null,
|
||||
createdAt: "",
|
||||
customGitBranch: "",
|
||||
customGitBuildPath: "",
|
||||
customGitSSHKeyId: null,
|
||||
customGitUrl: "",
|
||||
description: "",
|
||||
dockerfile: null,
|
||||
dockerImage: null,
|
||||
dropBuildPath: null,
|
||||
enabled: null,
|
||||
env: null,
|
||||
healthCheckSwarm: null,
|
||||
labelsSwarm: null,
|
||||
memoryLimit: null,
|
||||
memoryReservation: null,
|
||||
modeSwarm: null,
|
||||
mounts: [],
|
||||
name: "",
|
||||
networkSwarm: null,
|
||||
owner: null,
|
||||
password: null,
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
redirects: [],
|
||||
refreshToken: "",
|
||||
registry: null,
|
||||
registryId: null,
|
||||
replicas: 1,
|
||||
repository: null,
|
||||
restartPolicySwarm: null,
|
||||
rollbackConfigSwarm: null,
|
||||
security: [],
|
||||
sourceType: "git",
|
||||
subtitle: null,
|
||||
title: null,
|
||||
updateConfigSwarm: null,
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
};
|
||||
|
||||
const baseDomain: Domain = {
|
||||
applicationId: "",
|
||||
certificateType: "none",
|
||||
createdAt: "",
|
||||
domainId: "",
|
||||
host: "",
|
||||
https: false,
|
||||
path: null,
|
||||
port: null,
|
||||
serviceName: "",
|
||||
composeId: "",
|
||||
domainType: "application",
|
||||
uniqueConfigKey: 1,
|
||||
previewDeploymentId: "",
|
||||
};
|
||||
|
||||
const baseRedirect: Redirect = {
|
||||
redirectId: "",
|
||||
regex: "",
|
||||
replacement: "",
|
||||
permanent: false,
|
||||
uniqueConfigKey: 1,
|
||||
createdAt: "",
|
||||
applicationId: "",
|
||||
};
|
||||
|
||||
/** Middlewares */
|
||||
|
||||
test("Web entrypoint on http domain", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, https: false },
|
||||
"web",
|
||||
);
|
||||
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.rule).not.toContain("PathPrefix");
|
||||
});
|
||||
|
||||
test("Web entrypoint on http domain with custom path", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, path: "/foo", https: false },
|
||||
"web",
|
||||
);
|
||||
|
||||
expect(router.rule).toContain("PathPrefix(`/foo`)");
|
||||
});
|
||||
|
||||
test("Web entrypoint on http domain with redirect", async () => {
|
||||
const router = await createRouterConfig(
|
||||
{
|
||||
...baseApp,
|
||||
appName: "test",
|
||||
redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }],
|
||||
},
|
||||
{ ...baseDomain, https: false },
|
||||
"web",
|
||||
);
|
||||
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.middlewares).toContain("redirect-test-1");
|
||||
});
|
||||
|
||||
test("Web entrypoint on http domain with multiple redirect", async () => {
|
||||
const router = await createRouterConfig(
|
||||
{
|
||||
...baseApp,
|
||||
appName: "test",
|
||||
redirects: [
|
||||
{ ...baseRedirect, uniqueConfigKey: 1 },
|
||||
{ ...baseRedirect, uniqueConfigKey: 2 },
|
||||
],
|
||||
},
|
||||
{ ...baseDomain, https: false },
|
||||
"web",
|
||||
);
|
||||
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.middlewares).toContain("redirect-test-1");
|
||||
expect(router.middlewares).toContain("redirect-test-2");
|
||||
});
|
||||
|
||||
test("Web entrypoint on https domain", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, https: true },
|
||||
"web",
|
||||
);
|
||||
|
||||
expect(router.middlewares).toContain("redirect-to-https");
|
||||
});
|
||||
|
||||
test("Web entrypoint on https domain with redirect", async () => {
|
||||
const router = await createRouterConfig(
|
||||
{
|
||||
...baseApp,
|
||||
appName: "test",
|
||||
redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }],
|
||||
},
|
||||
{ ...baseDomain, https: true },
|
||||
"web",
|
||||
);
|
||||
|
||||
expect(router.middlewares).toContain("redirect-to-https");
|
||||
expect(router.middlewares).not.toContain("redirect-test-1");
|
||||
});
|
||||
|
||||
test("Websecure entrypoint on https domain", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, https: true },
|
||||
"websecure",
|
||||
);
|
||||
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
});
|
||||
|
||||
test("Websecure entrypoint on https domain with redirect", async () => {
|
||||
const router = await createRouterConfig(
|
||||
{
|
||||
...baseApp,
|
||||
appName: "test",
|
||||
redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }],
|
||||
},
|
||||
{ ...baseDomain, https: true },
|
||||
"websecure",
|
||||
);
|
||||
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.middlewares).toContain("redirect-test-1");
|
||||
});
|
||||
|
||||
/** Certificates */
|
||||
|
||||
test("CertificateType on websecure entrypoint", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, certificateType: "letsencrypt" },
|
||||
"websecure",
|
||||
);
|
||||
|
||||
expect(router.tls?.certResolver).toBe("letsencrypt");
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Cog } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
enum BuildType {
|
||||
dockerfile = "dockerfile",
|
||||
heroku_buildpacks = "heroku_buildpacks",
|
||||
paketo_buildpacks = "paketo_buildpacks",
|
||||
nixpacks = "nixpacks",
|
||||
static = "static",
|
||||
}
|
||||
|
||||
const mySchema = z.discriminatedUnion("buildType", [
|
||||
z.object({
|
||||
buildType: z.literal("dockerfile"),
|
||||
dockerfile: z
|
||||
.string({
|
||||
required_error: "Dockerfile path is required",
|
||||
invalid_type_error: "Dockerfile path is required",
|
||||
})
|
||||
.min(1, "Dockerfile required"),
|
||||
dockerContextPath: z.string().nullable().default(""),
|
||||
dockerBuildStage: z.string().nullable().default(""),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("heroku_buildpacks"),
|
||||
herokuVersion: z.string().nullable().default(""),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("paketo_buildpacks"),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("nixpacks"),
|
||||
publishDirectory: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("static"),
|
||||
}),
|
||||
]);
|
||||
|
||||
type AddTemplate = z.infer<typeof mySchema>;
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveBuildType.useMutation();
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm<AddTemplate>({
|
||||
defaultValues: {
|
||||
buildType: BuildType.nixpacks,
|
||||
},
|
||||
resolver: zodResolver(mySchema),
|
||||
});
|
||||
|
||||
const buildType = form.watch("buildType");
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data.buildType === "dockerfile") {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
...(data.buildType && {
|
||||
dockerfile: data.dockerfile || "",
|
||||
dockerContextPath: data.dockerContextPath || "",
|
||||
dockerBuildStage: data.dockerBuildStage || "",
|
||||
}),
|
||||
});
|
||||
} else if (data.buildType === "heroku_buildpacks") {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
...(data.buildType && {
|
||||
herokuVersion: data.herokuVersion || "",
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
publishDirectory: data.publishDirectory || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [form.formState.isSubmitSuccessful, form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: AddTemplate) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
buildType: data.buildType,
|
||||
publishDirectory:
|
||||
data.buildType === "nixpacks" ? data.publishDirectory : null,
|
||||
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
|
||||
dockerContextPath:
|
||||
data.buildType === "dockerfile" ? data.dockerContextPath : null,
|
||||
dockerBuildStage:
|
||||
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
|
||||
herokuVersion:
|
||||
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Build type saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the build type");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Build Type</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Select the way of building your code
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||
<Cog className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 p-2"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildType"
|
||||
defaultValue={form.control._defaultValues.buildType}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Build Type</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="dockerfile" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Dockerfile
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="nixpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Nixpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="heroku_buildpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Heroku Buildpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="paketo_buildpacks" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Paketo Buildpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="static" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Static</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{buildType === "heroku_buildpacks" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="herokuVersion"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Heroku Version (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Heroku Version (Default: 24)"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{buildType === "dockerfile" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerfile"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker File</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Path of your docker file"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerContextPath"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker Context Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={
|
||||
"Path of your docker context default: ."
|
||||
}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerBuildStage"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Docker Build Stage</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to target a specific stage in a
|
||||
Multi-stage Dockerfile. If empty, Docker defaults to
|
||||
build the last defined stage.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"E.g. production"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{buildType === "nixpacks" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="publishDirectory"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish Directory</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to serve a single directory via NGINX after
|
||||
the build phase. Useful if the final build assets
|
||||
should be served as a static site.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Publish Directory"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -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<typeof deleteApplicationSchema>;
|
||||
|
||||
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<DeleteApplication>({
|
||||
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 (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
application. If you are sure please enter the application name to
|
||||
delete this application.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-application"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter application name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-application"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,303 +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, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { domain } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Dices } from "lucide-react";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domain>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
domainId?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddDomain = ({
|
||||
applicationId,
|
||||
domainId = "",
|
||||
children,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { data, refetch } = api.domain.one.useQuery(
|
||||
{
|
||||
domainId,
|
||||
},
|
||||
{
|
||||
enabled: !!domainId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: application } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } = domainId
|
||||
? api.domain.update.useMutation()
|
||||
: api.domain.create.useMutation();
|
||||
|
||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||
api.domain.generateDomain.useMutation();
|
||||
|
||||
const form = useForm<Domain>({
|
||||
resolver: zodResolver(domain),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
...data,
|
||||
/* Convert null to undefined */
|
||||
path: data?.path || undefined,
|
||||
port: data?.port || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (!domainId) {
|
||||
form.reset({});
|
||||
}
|
||||
}, [form, form.reset, data, isLoading]);
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error to update the domain"
|
||||
: "Error to create the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
: "In this section you can add domains",
|
||||
};
|
||||
|
||||
const onSubmit = async (data: Domain) => {
|
||||
await mutateAsync({
|
||||
domainId,
|
||||
applicationId,
|
||||
...data,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(dictionary.success);
|
||||
await utils.domain.byApplicationId.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
await utils.application.readTraefikConfig.invalidate({ applicationId });
|
||||
|
||||
if (domainId) {
|
||||
refetch();
|
||||
}
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(dictionary.error);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input placeholder="api.dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingGenerate}
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
appName: application?.appName || "",
|
||||
serverId: application?.serverId || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
field.onChange(domain);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Dices className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate traefik.me domain</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="path"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"/"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{form.getValues().https && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||
{dictionary.submit}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Secrets } from "@/components/ui/secrets";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
env: z.string(),
|
||||
buildArgs: z.string(),
|
||||
});
|
||||
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveEnvironment.useMutation();
|
||||
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm<EnvironmentSchema>({
|
||||
defaultValues: {
|
||||
env: data?.env || "",
|
||||
buildArgs: data?.buildArgs || "",
|
||||
},
|
||||
resolver: zodResolver(addEnvironmentSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: EnvironmentSchema) => {
|
||||
mutateAsync({
|
||||
env: data.env,
|
||||
buildArgs: data.buildArgs,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Environments Added");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to add environment");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col gap-5 "
|
||||
>
|
||||
<Card className="bg-background p-6">
|
||||
<Secrets
|
||||
name="env"
|
||||
title="Environment Settings"
|
||||
description="You can add environment variables to your resource."
|
||||
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
|
||||
/>
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildArgs"
|
||||
title="Build-time Variables"
|
||||
description={
|
||||
<span>
|
||||
Available only at build-time. See documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/guide/build-args/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
<CardContent>
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,376 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const BitbucketProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||
});
|
||||
|
||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
|
||||
api.application.saveBitbucketProvider.useMutation();
|
||||
|
||||
const form = useForm<BitbucketProvider>({
|
||||
defaultValues: {
|
||||
buildPath: "/",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
bitbucketId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(BitbucketProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const bitbucketId = form.watch("bitbucketId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
isError,
|
||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||
{
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!bitbucketId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.bitbucket.getBitbucketBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.bitbucketBranch || "",
|
||||
repository: {
|
||||
repo: data.bitbucketRepository || "",
|
||||
owner: data.bitbucketOwner || "",
|
||||
},
|
||||
buildPath: data.bitbucketBuildPath || "/",
|
||||
bitbucketId: data.bitbucketId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: BitbucketProvider) => {
|
||||
await mutateAsync({
|
||||
bitbucketBranch: data.branch,
|
||||
bitbucketRepository: data.repository.repo,
|
||||
bitbucketOwner: data.repository.owner,
|
||||
bitbucketBuildPath: data.buildPath,
|
||||
bitbucketId: data.bitbucketId,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Bitbucket provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
{error && (
|
||||
<AlertBlock type="error">Repositories: {error.message}</AlertBlock>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bitbucketId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Bitbucket Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Bitbucket Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{bitbucketProviders?.map((bitbucketProvider) => (
|
||||
<SelectItem
|
||||
key={bitbucketProvider.bitbucketId}
|
||||
value={bitbucketProvider.bitbucketId}
|
||||
>
|
||||
{bitbucketProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username as string,
|
||||
repo: repo.name,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.repository && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Repository is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="block w-full">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
: "Select branch"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
className="h-9"
|
||||
/>
|
||||
{status === "loading" && fetchStatus === "fetching" && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Loading Branches....
|
||||
</span>
|
||||
)}
|
||||
{!repository?.owner && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a repository
|
||||
</span>
|
||||
)}
|
||||
<ScrollArea className="h-96">
|
||||
<CommandEmpty>No branch found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{branches?.map((branch) => (
|
||||
<CommandItem
|
||||
value={branch.name}
|
||||
key={branch.commit.sha}
|
||||
onSelect={() => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
branch.name === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
<FormMessage />
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
isLoading={isSavingBitbucketProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,141 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dropzone } from "@/components/ui/dropzone";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { type UploadFile, uploadFileSchema } from "@/utils/schema";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const SaveDragNDrop = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.dropDeployment.useMutation();
|
||||
|
||||
const form = useForm<UploadFile>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(uploadFileSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
dropBuildPath: data.dropBuildPath || "",
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
const zip = form.watch("zip");
|
||||
|
||||
const onSubmit = async (values: UploadFile) => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("zip", values.zip);
|
||||
formData.append("applicationId", applicationId);
|
||||
if (values.dropBuildPath) {
|
||||
formData.append("dropBuildPath", values.dropBuildPath);
|
||||
}
|
||||
|
||||
await mutateAsync(formData)
|
||||
.then(async () => {
|
||||
toast.success("Deployment saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the deployment");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-4 ">
|
||||
<div className="md:col-span-2 space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dropBuildPath"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full ">
|
||||
<FormLabel>Build Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Build Path" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="zip"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full ">
|
||||
<FormLabel>Zip file</FormLabel>
|
||||
<FormControl>
|
||||
<Dropzone
|
||||
{...field}
|
||||
dropMessage="Drop files or click here"
|
||||
accept=".zip"
|
||||
onChange={(e) => {
|
||||
if (e instanceof FileList) {
|
||||
field.onChange(e[0]);
|
||||
} else {
|
||||
field.onChange(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{zip instanceof File && (
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{zip.name} ({zip.size} bytes)
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-fit"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
field.onChange(null);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
isLoading={isLoading}
|
||||
disabled={!zip || isLoading}
|
||||
>
|
||||
Deploy{" "}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,199 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { KeyRoundIcon, LockIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const GitProviderSchema = z.object({
|
||||
repositoryURL: z.string().min(1, {
|
||||
message: "Repository URL is required",
|
||||
}),
|
||||
branch: z.string().min(1, "Branch required"),
|
||||
buildPath: z.string().min(1, "Build Path required"),
|
||||
sshKey: z.string().optional(),
|
||||
});
|
||||
|
||||
type GitProvider = z.infer<typeof GitProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveGitProdiver.useMutation();
|
||||
|
||||
const form = useForm<GitProvider>({
|
||||
defaultValues: {
|
||||
branch: "",
|
||||
buildPath: "/",
|
||||
repositoryURL: "",
|
||||
sshKey: undefined,
|
||||
},
|
||||
resolver: zodResolver(GitProviderSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
sshKey: data.customGitSSHKeyId || undefined,
|
||||
branch: data.customGitBranch || "",
|
||||
buildPath: data.customGitBuildPath || "/",
|
||||
repositoryURL: data.customGitUrl || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (values: GitProvider) => {
|
||||
await mutateAsync({
|
||||
customGitBranch: values.branch,
|
||||
customGitBuildPath: values.buildPath,
|
||||
customGitUrl: values.repositoryURL,
|
||||
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Git Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Git provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="flex items-end col-span-2 gap-4">
|
||||
<div className="grow">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repositoryURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Repository URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="git@bitbucket.org" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{sshKeys && sshKeys.length > 0 ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="basis-40">
|
||||
<FormLabel className="w-full inline-flex justify-between">
|
||||
SSH Key
|
||||
<LockIcon className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
key={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{sshKeys?.map((sshKey) => (
|
||||
<SelectItem
|
||||
key={sshKey.sshKeyId}
|
||||
value={sshKey.sshKeyId}
|
||||
>
|
||||
{sshKey.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => router.push("/dashboard/settings/ssh-keys")}
|
||||
type="button"
|
||||
>
|
||||
<KeyRoundIcon className="size-4" /> Add SSH Key
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Branch" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button type="submit" className="w-fit" isLoading={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,392 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const GitlabProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
gitlabPathNamespace: z.string().min(1),
|
||||
id: z.number().nullable(),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||
});
|
||||
|
||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGitlabProvider } =
|
||||
api.application.saveGitlabProvider.useMutation();
|
||||
|
||||
const form = useForm<GitlabProvider>({
|
||||
defaultValues: {
|
||||
buildPath: "/",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
gitlabPathNamespace: "",
|
||||
id: null,
|
||||
},
|
||||
gitlabId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GitlabProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const gitlabId = form.watch("gitlabId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
} = api.gitlab.getGitlabRepositories.useQuery(
|
||||
{
|
||||
gitlabId,
|
||||
},
|
||||
{
|
||||
enabled: !!gitlabId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.gitlab.getGitlabBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
id: repository?.id || 0,
|
||||
gitlabId: gitlabId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!gitlabId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.gitlabBranch || "",
|
||||
repository: {
|
||||
repo: data.gitlabRepository || "",
|
||||
owner: data.gitlabOwner || "",
|
||||
gitlabPathNamespace: data.gitlabPathNamespace || "",
|
||||
id: data.gitlabProjectId,
|
||||
},
|
||||
buildPath: data.gitlabBuildPath || "/",
|
||||
gitlabId: data.gitlabId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GitlabProvider) => {
|
||||
await mutateAsync({
|
||||
gitlabBranch: data.branch,
|
||||
gitlabRepository: data.repository.repo,
|
||||
gitlabOwner: data.repository.owner,
|
||||
gitlabBuildPath: data.buildPath,
|
||||
gitlabId: data.gitlabId,
|
||||
applicationId,
|
||||
gitlabProjectId: data.repository.id,
|
||||
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the gitlab provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="gitlabId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Gitlab Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
id: null,
|
||||
gitlabPathNamespace: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Gitlab Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{gitlabProviders?.map((gitlabProvider) => (
|
||||
<SelectItem
|
||||
key={gitlabProvider.gitlabId}
|
||||
value={gitlabProvider.gitlabId}
|
||||
>
|
||||
{gitlabProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{repositories && repositories.length === 0 && (
|
||||
<CommandEmpty>
|
||||
No repositories found.
|
||||
</CommandEmpty>
|
||||
)}
|
||||
{repositories?.map((repo) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username as string,
|
||||
repo: repo.name,
|
||||
id: repo.id,
|
||||
gitlabPathNamespace: repo.url,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.repository && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Repository is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="block w-full">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
: "Select branch"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
className="h-9"
|
||||
/>
|
||||
{status === "loading" && fetchStatus === "fetching" && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Loading Branches....
|
||||
</span>
|
||||
)}
|
||||
{!repository?.owner && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a repository
|
||||
</span>
|
||||
)}
|
||||
<ScrollArea className="h-96">
|
||||
<CommandEmpty>No branch found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{branches?.map((branch) => (
|
||||
<CommandItem
|
||||
value={branch.name}
|
||||
key={branch.commit.id}
|
||||
onSelect={() => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
branch.name === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
<FormMessage />
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
isLoading={isSavingGitlabProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,179 +0,0 @@
|
||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
||||
import {
|
||||
BitbucketIcon,
|
||||
DockerIcon,
|
||||
GitIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
import { GitBranch, LockIcon, UploadCloud } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||
|
||||
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
|
||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Select the source of your code
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={tab}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GithubIcon className="size-4 text-current fill-current" />
|
||||
Github
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitlab"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitlabIcon className="size-4 text-current fill-current" />
|
||||
Gitlab
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="bitbucket"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||
Bitbucket
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="docker"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<DockerIcon className="size-5 text-current" />
|
||||
Docker
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="git"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitIcon />
|
||||
Git
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="drop"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<UploadCloud className="size-5 text-current" />
|
||||
Drop
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{githubProviders && githubProviders?.length > 0 ? (
|
||||
<SaveGithubProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GithubIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using GitHub, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitlab" className="w-full p-2">
|
||||
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
||||
<SaveGitlabProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GitlabIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using GitLab, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="bitbucket" className="w-full p-2">
|
||||
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
||||
<SaveBitbucketProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<BitbucketIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using Bitbucket, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="docker" className="w-full p-2">
|
||||
<SaveDockerProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="drop" className="w-full p-2">
|
||||
<SaveDragNDrop applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,304 +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, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { domain } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Dices } from "lucide-react";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domain>;
|
||||
|
||||
interface Props {
|
||||
previewDeploymentId: string;
|
||||
domainId?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddPreviewDomain = ({
|
||||
previewDeploymentId,
|
||||
domainId = "",
|
||||
children,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { data, refetch } = api.domain.one.useQuery(
|
||||
{
|
||||
domainId,
|
||||
},
|
||||
{
|
||||
enabled: !!domainId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: previewDeployment } = api.previewDeployment.one.useQuery(
|
||||
{
|
||||
previewDeploymentId,
|
||||
},
|
||||
{
|
||||
enabled: !!previewDeploymentId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } = domainId
|
||||
? api.domain.update.useMutation()
|
||||
: api.domain.create.useMutation();
|
||||
|
||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||
api.domain.generateDomain.useMutation();
|
||||
|
||||
const form = useForm<Domain>({
|
||||
resolver: zodResolver(domain),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
...data,
|
||||
/* Convert null to undefined */
|
||||
path: data?.path || undefined,
|
||||
port: data?.port || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (!domainId) {
|
||||
form.reset({});
|
||||
}
|
||||
}, [form, form.reset, data, isLoading]);
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error to update the domain"
|
||||
: "Error to create the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
: "In this section you can add domains",
|
||||
};
|
||||
|
||||
const onSubmit = async (data: Domain) => {
|
||||
await mutateAsync({
|
||||
domainId,
|
||||
previewDeploymentId,
|
||||
...data,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(dictionary.success);
|
||||
await utils.previewDeployment.all.invalidate({
|
||||
applicationId: previewDeployment?.applicationId,
|
||||
});
|
||||
|
||||
if (domainId) {
|
||||
refetch();
|
||||
}
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(dictionary.error);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input placeholder="api.dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingGenerate}
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
appName: previewDeployment?.appName || "",
|
||||
serverId:
|
||||
previewDeployment?.application
|
||||
?.serverId || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
field.onChange(domain);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Dices className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate traefik.me domain</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="path"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"/"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{form.getValues().https && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||
{dictionary.submit}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import type { RouterOutputs } from "@/utils/api";
|
||||
import { useState } from "react";
|
||||
import { ShowDeployment } from "../deployments/show-deployment";
|
||||
|
||||
interface Props {
|
||||
deployments: RouterOutputs["deployment"]["all"];
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
|
||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">View Builds</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Preview Builds</DialogTitle>
|
||||
<DialogDescription>
|
||||
See all the preview builds for this application on this Pull Request
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
{deployments?.map((deployment) => (
|
||||
<div
|
||||
key={deployment.deploymentId}
|
||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||
{deployment.status}
|
||||
|
||||
<StatusTooltip
|
||||
status={deployment?.status}
|
||||
className="size-2.5"
|
||||
/>
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{deployment.title}
|
||||
</span>
|
||||
{deployment.description && (
|
||||
<span className="break-all text-sm text-muted-foreground">
|
||||
{deployment.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="text-sm capitalize text-muted-foreground">
|
||||
<DateTooltip date={deployment.createdAt} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveLog(deployment.logPath);
|
||||
}}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<ShowDeployment
|
||||
serverId={serverId || ""}
|
||||
open={activeLog !== null}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,212 +0,0 @@
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { Pencil, RocketIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } 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";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
const [activeLog, setActiveLog] = useState<string | null>(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 },
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
// const [url, setUrl] = React.useState("");
|
||||
// useEffect(() => {
|
||||
// setUrl(document.location.origin);
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Preview Deployments</CardTitle>
|
||||
<CardDescription>See all the preview deployments</CardDescription>
|
||||
</div>
|
||||
{data?.isPreviewDeploymentsActive && (
|
||||
<ShowPreviewSettings applicationId={applicationId} />
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{data?.isPreviewDeploymentsActive ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<span>
|
||||
Preview deployments are a way to test your application before it
|
||||
is deployed to production. It will create a new deployment for
|
||||
each pull request you create.
|
||||
</span>
|
||||
</div>
|
||||
{data?.previewDeployments?.length === 0 ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<RocketIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No preview deployments found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{previewDeployments?.map((previewDeployment) => {
|
||||
const { deployments, domain } = previewDeployment;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={previewDeployment?.previewDeploymentId}
|
||||
className="flex flex-col justify-between rounded-lg border p-4 gap-2"
|
||||
>
|
||||
<div className="flex justify-between gap-2 max-sm:flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
{deployments?.length === 0 ? (
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
No deployments found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||
{previewDeployment?.pullRequestTitle}
|
||||
</span>
|
||||
<StatusTooltip
|
||||
status={previewDeployment.previewStatus}
|
||||
className="size-2.5"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
{previewDeployment?.pullRequestTitle && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="break-all text-sm text-muted-foreground w-fit">
|
||||
Title: {previewDeployment?.pullRequestTitle}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewDeployment?.pullRequestURL && (
|
||||
<div className="flex items-center gap-2">
|
||||
<GithubIcon />
|
||||
<Link
|
||||
target="_blank"
|
||||
href={previewDeployment?.pullRequestURL}
|
||||
className="break-all text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
||||
>
|
||||
Pull Request URL
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ">
|
||||
<span>Domain </span>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`http://${domain?.host}`}
|
||||
className="text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
||||
>
|
||||
{domain?.host}
|
||||
</Link>
|
||||
<AddPreviewDomain
|
||||
previewDeploymentId={
|
||||
previewDeployment.previewDeploymentId
|
||||
}
|
||||
domainId={domain?.domainId}
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AddPreviewDomain>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:items-end gap-2 max-sm:w-full">
|
||||
{previewDeployment?.createdAt && (
|
||||
<div className="text-sm capitalize text-muted-foreground">
|
||||
<DateTooltip
|
||||
date={previewDeployment?.createdAt}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ShowPreviewBuilds
|
||||
deployments={previewDeployment?.deployments || []}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
|
||||
<ShowModalLogs
|
||||
appName={previewDeployment.appName}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">View Logs</Button>
|
||||
</ShowModalLogs>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Preview"
|
||||
description="Are you sure you want to delete this preview?"
|
||||
onClick={() => {
|
||||
deletePreviewDeployment({
|
||||
previewDeploymentId:
|
||||
previewDeployment.previewDeploymentId,
|
||||
})
|
||||
.then(() => {
|
||||
refetchPreviewDeployments();
|
||||
toast.success("Preview deployment deleted");
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="destructive" isLoading={isLoading}>
|
||||
Delete Preview
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<RocketIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
Preview deployments are disabled for this application, please
|
||||
enable it
|
||||
</span>
|
||||
<ShowPreviewSettings applicationId={applicationId} />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,351 +0,0 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
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, 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,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const schema = z.object({
|
||||
env: z.string(),
|
||||
buildArgs: z.string(),
|
||||
wildcardDomain: z.string(),
|
||||
port: z.number(),
|
||||
previewLimit: z.number(),
|
||||
previewHttps: z.boolean(),
|
||||
previewPath: z.string(),
|
||||
previewCertificateType: z.enum(["letsencrypt", "none"]),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof schema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const { mutateAsync: updateApplication, isLoading } =
|
||||
api.application.update.useMutation();
|
||||
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
env: "",
|
||||
wildcardDomain: "*.traefik.me",
|
||||
port: 3000,
|
||||
previewLimit: 3,
|
||||
previewHttps: false,
|
||||
previewPath: "/",
|
||||
previewCertificateType: "none",
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
const previewHttps = form.watch("previewHttps");
|
||||
|
||||
useEffect(() => {
|
||||
setIsEnabled(data?.isPreviewDeploymentsActive || false);
|
||||
}, [data?.isPreviewDeploymentsActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
env: data.previewEnv || "",
|
||||
buildArgs: data.previewBuildArgs || "",
|
||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||
port: data.previewPort || 3000,
|
||||
previewLimit: data.previewLimit || 3,
|
||||
previewHttps: data.previewHttps || false,
|
||||
previewPath: data.previewPath || "/",
|
||||
previewCertificateType: data.previewCertificateType || "none",
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onSubmit = async (formData: Schema) => {
|
||||
updateApplication({
|
||||
previewEnv: formData.env,
|
||||
previewBuildArgs: formData.buildArgs,
|
||||
previewWildcard: formData.wildcardDomain,
|
||||
previewPort: formData.port,
|
||||
applicationId,
|
||||
previewLimit: formData.previewLimit,
|
||||
previewHttps: formData.previewHttps,
|
||||
previewPath: formData.previewPath,
|
||||
previewCertificateType: formData.previewCertificateType,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Preview Deployments settings updated");
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">View Settings</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Preview Deployment Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Adjust the settings for preview deployments of this application,
|
||||
including environment variables, build options, and deployment
|
||||
rules.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-application"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="wildcardDomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Wildcard Domain</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="*.traefik.me" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Preview Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder="3000" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewLimit"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Preview Limit</FormLabel>
|
||||
{/* <FormDescription>
|
||||
Set the limit of preview deployments that can be
|
||||
created for this app.
|
||||
</FormDescription> */}
|
||||
<FormControl>
|
||||
<NumberInput placeholder="3000" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewHttps"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{previewHttps && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewCertificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="flex flex-row items-center justify-between rounded-lg border p-4 col-span-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Enable preview deployments
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Enable or disable preview deployments for this
|
||||
application.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
updateApplication({
|
||||
isPreviewDeploymentsActive: checked,
|
||||
applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success("Preview deployments enabled");
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="env"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Secrets
|
||||
name="env"
|
||||
title="Environment Settings"
|
||||
description="You can add environment variables to your resource."
|
||||
placeholder={[
|
||||
"NODE_ENV=production",
|
||||
"PORT=3000",
|
||||
].join("\n")}
|
||||
/>
|
||||
{/* <CodeEditor
|
||||
lineWrapping
|
||||
language="properties"
|
||||
wrapperClassName="h-[25rem] font-mono"
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
`}
|
||||
{...field}
|
||||
/> */}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildArgs"
|
||||
title="Build-time Variables"
|
||||
description={
|
||||
<span>
|
||||
Available only at build-time. See documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/guide/build-args/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-application"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<typeof deleteComposeSchema>;
|
||||
|
||||
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<DeleteCompose>({
|
||||
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 (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
compose. If you are sure please enter the compose name to delete
|
||||
this compose.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-delete-compose"
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="projectName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
||||
below
|
||||
</FormLabel>{" "}
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter compose name to confirm"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-delete-compose"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,443 +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, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { domainCompose } from "@/server/db/validations/domain";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||
import type z from "zod";
|
||||
|
||||
type Domain = z.infer<typeof domainCompose>;
|
||||
|
||||
export type CacheType = "fetch" | "cache";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
domainId?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddDomainCompose = ({
|
||||
composeId,
|
||||
domainId = "",
|
||||
children,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||
const utils = api.useUtils();
|
||||
const { data, refetch } = api.domain.one.useQuery(
|
||||
{
|
||||
domainId,
|
||||
},
|
||||
{
|
||||
enabled: !!domainId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: compose } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{
|
||||
enabled: !!composeId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: services,
|
||||
isFetching: isLoadingServices,
|
||||
error: errorServices,
|
||||
refetch: refetchServices,
|
||||
} = api.compose.loadServices.useQuery(
|
||||
{
|
||||
composeId,
|
||||
type: cacheType,
|
||||
},
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||
api.domain.generateDomain.useMutation();
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } = domainId
|
||||
? api.domain.update.useMutation()
|
||||
: api.domain.create.useMutation();
|
||||
|
||||
const form = useForm<Domain>({
|
||||
resolver: zodResolver(domainCompose),
|
||||
});
|
||||
|
||||
const https = form.watch("https");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
...data,
|
||||
/* Convert null to undefined */
|
||||
path: data?.path || undefined,
|
||||
port: data?.port || undefined,
|
||||
serviceName: data?.serviceName || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (!domainId) {
|
||||
form.reset({});
|
||||
}
|
||||
}, [form, form.reset, data, isLoading]);
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error to update the domain"
|
||||
: "Error to create the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
: "In this section you can add domains",
|
||||
};
|
||||
|
||||
const onSubmit = async (data: Domain) => {
|
||||
await mutateAsync({
|
||||
domainId,
|
||||
composeId,
|
||||
domainType: "compose",
|
||||
...data,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.domain.byComposeId.invalidate({
|
||||
composeId,
|
||||
});
|
||||
toast.success(dictionary.success);
|
||||
if (domainId) {
|
||||
refetch();
|
||||
}
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(dictionary.error);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
Deploy is required to apply changes after creating or updating a
|
||||
domain.
|
||||
</AlertBlock>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{errorServices && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="[overflow-wrap:anywhere]"
|
||||
>
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
<div className="flex flex-row items-end w-full gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serviceName"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Service Name</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a service name" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
{services?.map((service, index) => (
|
||||
<SelectItem
|
||||
value={service}
|
||||
key={`${service}-${index}`}
|
||||
>
|
||||
{service}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none" disabled>
|
||||
Empty
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingServices}
|
||||
onClick={() => {
|
||||
if (cacheType === "fetch") {
|
||||
refetchServices();
|
||||
} else {
|
||||
setCacheType("fetch");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
services
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingServices}
|
||||
onClick={() => {
|
||||
if (cacheType === "cache") {
|
||||
refetchServices();
|
||||
} else {
|
||||
setCacheType("cache");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this
|
||||
compose, it will read the services from the
|
||||
last deployment/fetch from the repository
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input placeholder="api.dokploy.com" {...field} />
|
||||
</FormControl>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
isLoading={isLoadingGenerate}
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
serverId: compose?.serverId || "",
|
||||
appName: compose?.appName || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
field.onChange(domain);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Dices className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate traefik.me domain</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="path"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"/"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{https && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certificateType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Certificate</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value || ""}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a certificate" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value={"letsencrypt"}>
|
||||
Letsencrypt (Default)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
>
|
||||
{dictionary.submit}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { DeleteDomain } from "../../application/domains/delete-domain";
|
||||
import { AddDomainCompose } from "./add-domain";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
const { data } = api.domain.byComposeId.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
{
|
||||
enabled: !!composeId,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">Domains</CardTitle>
|
||||
<CardDescription>
|
||||
Domains are used to access to the application
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
{data && data?.length > 0 && (
|
||||
<AddDomainCompose composeId={composeId}>
|
||||
<Button>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</Button>
|
||||
</AddDomainCompose>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-row gap-4">
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To access to the application it is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
<AddDomainCompose composeId={composeId}>
|
||||
<Button>
|
||||
<GlobeIcon className="size-4" /> Add Domain
|
||||
</Button>
|
||||
</AddDomainCompose>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{data?.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.domainId}
|
||||
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
||||
>
|
||||
<Link target="_blank" href={`http://${item.host}`}>
|
||||
<ExternalLink className="size-5" />
|
||||
</Link>
|
||||
<Button variant="outline" disabled>
|
||||
{item.serviceName}
|
||||
</Button>
|
||||
<Input disabled value={item.host} />
|
||||
<Button variant="outline" disabled>
|
||||
{item.path}
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
{item.port}
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
{item.https ? "HTTPS" : "HTTP"}
|
||||
</Button>
|
||||
<div className="flex flex-row gap-1">
|
||||
<AddDomainCompose
|
||||
composeId={composeId}
|
||||
domainId={item.domainId}
|
||||
>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AddDomainCompose>
|
||||
<DeleteDomain domainId={item.domainId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,378 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const BitbucketProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||
});
|
||||
|
||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
|
||||
api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<BitbucketProvider>({
|
||||
defaultValues: {
|
||||
composePath: "./docker-compose.yml",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
bitbucketId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(BitbucketProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const bitbucketId = form.watch("bitbucketId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
isError,
|
||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||
{
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!bitbucketId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.bitbucket.getBitbucketBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.bitbucketBranch || "",
|
||||
repository: {
|
||||
repo: data.bitbucketRepository || "",
|
||||
owner: data.bitbucketOwner || "",
|
||||
},
|
||||
composePath: data.composePath,
|
||||
bitbucketId: data.bitbucketId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: BitbucketProvider) => {
|
||||
await mutateAsync({
|
||||
bitbucketBranch: data.branch,
|
||||
bitbucketRepository: data.repository.repo,
|
||||
bitbucketOwner: data.repository.owner,
|
||||
bitbucketId: data.bitbucketId,
|
||||
composePath: data.composePath,
|
||||
composeId,
|
||||
sourceType: "bitbucket",
|
||||
composeStatus: "idle",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Bitbucket provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
{error && (
|
||||
<AlertBlock type="error">Repositories: {error.message}</AlertBlock>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bitbucketId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Bitbucket Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Bitbucket Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{bitbucketProviders?.map((bitbucketProvider) => (
|
||||
<SelectItem
|
||||
key={bitbucketProvider.bitbucketId}
|
||||
value={bitbucketProvider.bitbucketId}
|
||||
>
|
||||
{bitbucketProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username as string,
|
||||
repo: repo.name,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.repository && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Repository is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="block w-full">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
: "Select branch"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
className="h-9"
|
||||
/>
|
||||
{status === "loading" && fetchStatus === "fetching" && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Loading Branches....
|
||||
</span>
|
||||
)}
|
||||
{!repository?.owner && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a repository
|
||||
</span>
|
||||
)}
|
||||
<ScrollArea className="h-96">
|
||||
<CommandEmpty>No branch found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{branches?.map((branch) => (
|
||||
<CommandItem
|
||||
value={branch.name}
|
||||
key={branch.commit.sha}
|
||||
onSelect={() => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
branch.name === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
<FormMessage />
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Compose Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="docker-compose.yml" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
isLoading={isSavingBitbucketProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,204 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { KeyRoundIcon, LockIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const GitProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
repositoryURL: z.string().min(1, {
|
||||
message: "Repository URL is required",
|
||||
}),
|
||||
branch: z.string().min(1, "Branch required"),
|
||||
sshKey: z.string().optional(),
|
||||
});
|
||||
|
||||
type GitProvider = z.infer<typeof GitProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<GitProvider>({
|
||||
defaultValues: {
|
||||
branch: "",
|
||||
repositoryURL: "",
|
||||
composePath: "./docker-compose.yml",
|
||||
sshKey: undefined,
|
||||
},
|
||||
resolver: zodResolver(GitProviderSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
sshKey: data.customGitSSHKeyId || undefined,
|
||||
branch: data.customGitBranch || "",
|
||||
repositoryURL: data.customGitUrl || "",
|
||||
composePath: data.composePath,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (values: GitProvider) => {
|
||||
await mutateAsync({
|
||||
customGitBranch: values.branch,
|
||||
customGitUrl: values.repositoryURL,
|
||||
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
|
||||
composeId,
|
||||
sourceType: "git",
|
||||
composePath: values.composePath,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Git Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Git provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-4 ">
|
||||
<div className="flex items-end col-span-2 gap-4">
|
||||
<div className="grow">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repositoryURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row justify-between">
|
||||
Repository URL
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="git@bitbucket.org" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{sshKeys && sshKeys.length > 0 ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="basis-40">
|
||||
<FormLabel className="w-full inline-flex justify-between">
|
||||
SSH Key
|
||||
<LockIcon className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
key={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{sshKeys?.map((sshKey) => (
|
||||
<SelectItem
|
||||
key={sshKey.sshKeyId}
|
||||
value={sshKey.sshKeyId}
|
||||
>
|
||||
{sshKey.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => router.push("/dashboard/settings/ssh-keys")}
|
||||
type="button"
|
||||
>
|
||||
<KeyRoundIcon className="size-4" /> Add SSH Key
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Branch" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Compose Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="docker-compose.yml" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button type="submit" className="w-fit" isLoading={isLoading}>
|
||||
Save{" "}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,394 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const GitlabProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
id: z.number().nullable(),
|
||||
gitlabPathNamespace: z.string().min(1),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||
});
|
||||
|
||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGitlabProvider } =
|
||||
api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<GitlabProvider>({
|
||||
defaultValues: {
|
||||
composePath: "./docker-compose.yml",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
gitlabPathNamespace: "",
|
||||
id: null,
|
||||
},
|
||||
gitlabId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GitlabProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const gitlabId = form.watch("gitlabId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
} = api.gitlab.getGitlabRepositories.useQuery(
|
||||
{
|
||||
gitlabId,
|
||||
},
|
||||
{
|
||||
enabled: !!gitlabId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.gitlab.getGitlabBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
id: repository?.id || 0,
|
||||
gitlabId: gitlabId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!gitlabId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.gitlabBranch || "",
|
||||
repository: {
|
||||
repo: data.gitlabRepository || "",
|
||||
owner: data.gitlabOwner || "",
|
||||
id: data.gitlabProjectId,
|
||||
gitlabPathNamespace: data.gitlabPathNamespace || "",
|
||||
},
|
||||
composePath: data.composePath,
|
||||
gitlabId: data.gitlabId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GitlabProvider) => {
|
||||
await mutateAsync({
|
||||
gitlabBranch: data.branch,
|
||||
gitlabRepository: data.repository.repo,
|
||||
gitlabOwner: data.repository.owner,
|
||||
composePath: data.composePath,
|
||||
gitlabId: data.gitlabId,
|
||||
composeId,
|
||||
gitlabProjectId: data.repository.id,
|
||||
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
||||
sourceType: "gitlab",
|
||||
composeStatus: "idle",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the gitlab provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="gitlabId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Gitlab Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
gitlabPathNamespace: "",
|
||||
id: null,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Gitlab Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{gitlabProviders?.map((gitlabProvider) => (
|
||||
<SelectItem
|
||||
key={gitlabProvider.gitlabId}
|
||||
value={gitlabProvider.gitlabId}
|
||||
>
|
||||
{gitlabProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{repositories && repositories.length === 0 && (
|
||||
<CommandEmpty>
|
||||
No repositories found.
|
||||
</CommandEmpty>
|
||||
)}
|
||||
{repositories?.map((repo) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username as string,
|
||||
repo: repo.name,
|
||||
id: repo.id,
|
||||
gitlabPathNamespace: repo.url,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.repository && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Repository is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="block w-full">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
: "Select branch"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
className="h-9"
|
||||
/>
|
||||
{status === "loading" && fetchStatus === "fetching" && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Loading Branches....
|
||||
</span>
|
||||
)}
|
||||
{!repository?.owner && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a repository
|
||||
</span>
|
||||
)}
|
||||
<ScrollArea className="h-96">
|
||||
<CommandEmpty>No branch found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{branches?.map((branch) => (
|
||||
<CommandItem
|
||||
value={branch.name}
|
||||
key={branch.commit.id}
|
||||
onSelect={() => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
branch.name === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
<FormMessage />
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Compose Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="docker-compose.yml" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
isLoading={isSavingGitlabProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user