mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
1 Commits
v0.18.3
...
migration/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eaafcb572 |
119
.circleci/config.yml
Normal file
119
.circleci/config.yml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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
|
||||||
BIN
.github/sponsors/its.png
vendored
BIN
.github/sponsors/its.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
BIN
.github/sponsors/light-node.webp
vendored
BIN
.github/sponsors/light-node.webp
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 KiB |
BIN
.github/sponsors/openalternative.png
vendored
BIN
.github/sponsors/openalternative.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 KiB |
83
.github/workflows/create-pr.yml
vendored
83
.github/workflows/create-pr.yml
vendored
@@ -1,83 +0,0 @@
|
|||||||
name: Auto PR to main when version changes
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- canary
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-pr:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Get version from package.json
|
|
||||||
id: package_version
|
|
||||||
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Get latest GitHub tag
|
|
||||||
id: latest_tag
|
|
||||||
run: |
|
|
||||||
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1)
|
|
||||||
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
|
||||||
echo $LATEST_TAG
|
|
||||||
- name: Compare versions
|
|
||||||
id: compare_versions
|
|
||||||
run: |
|
|
||||||
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
|
|
||||||
VERSION_CHANGED="true"
|
|
||||||
else
|
|
||||||
VERSION_CHANGED="false"
|
|
||||||
fi
|
|
||||||
echo "VERSION_CHANGED=$VERSION_CHANGED" >> $GITHUB_ENV
|
|
||||||
echo "Comparing versions:"
|
|
||||||
echo "Current version: ${{ env.VERSION }}"
|
|
||||||
echo "Latest tag: ${{ env.LATEST_TAG }}"
|
|
||||||
echo "Version changed: $VERSION_CHANGED"
|
|
||||||
- name: Check if a PR already exists
|
|
||||||
id: check_pr
|
|
||||||
run: |
|
|
||||||
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
|
|
||||||
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
if: env.VERSION_CHANGED == 'true' && env.PR_EXISTS == '0'
|
|
||||||
run: |
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
git fetch origin main
|
|
||||||
git checkout canary
|
|
||||||
git push origin canary
|
|
||||||
|
|
||||||
gh pr create \
|
|
||||||
--title "🚀 Release ${{ env.VERSION }}" \
|
|
||||||
--body '
|
|
||||||
This PR promotes changes from `canary` to `main` for version ${{ env.VERSION }}.
|
|
||||||
|
|
||||||
### 🔍 Changes Include:
|
|
||||||
- Version bump to ${{ env.VERSION }}
|
|
||||||
- All changes from canary branch
|
|
||||||
|
|
||||||
### ✅ Pre-merge Checklist:
|
|
||||||
- [ ] All tests passing
|
|
||||||
- [ ] Documentation updated
|
|
||||||
- [ ] Docker images built and tested
|
|
||||||
|
|
||||||
> 🤖 This PR was automatically generated by [GitHub Actions](https://github.com/actions)' \
|
|
||||||
--base main \
|
|
||||||
--head canary \
|
|
||||||
--label "release" --label "automated pr" || true \
|
|
||||||
--reviewer siumauricio \
|
|
||||||
--assignee siumauricio
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
9
.github/workflows/deploy.yml
vendored
9
.github/workflows/deploy.yml
vendored
@@ -2,7 +2,7 @@ name: Build Docker images
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["canary", "main", "feat/monitoring"]
|
branches: ["canary", "main"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-cloud-image:
|
build-and-push-cloud-image:
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -53,7 +53,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
|
||||||
build-and-push-server-image:
|
build-and-push-server-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -76,4 +77,4 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
161
.github/workflows/dokploy.yml
vendored
161
.github/workflows/dokploy.yml
vendored
@@ -1,161 +0,0 @@
|
|||||||
name: Dokploy Docker Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, canary, "feat/monitoring"]
|
|
||||||
|
|
||||||
env:
|
|
||||||
IMAGE_NAME: dokploy/dokploy
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker-amd:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set tag and version
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
|
||||||
TAG="latest"
|
|
||||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
|
||||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
|
||||||
TAG="canary"
|
|
||||||
else
|
|
||||||
TAG="feature"
|
|
||||||
fi
|
|
||||||
echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Prepare env file
|
|
||||||
run: |
|
|
||||||
cp apps/dokploy/.env.production.example .env.production
|
|
||||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
docker-arm:
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set tag and version
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
|
||||||
TAG="latest"
|
|
||||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
|
||||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
|
||||||
TAG="canary"
|
|
||||||
else
|
|
||||||
TAG="feature"
|
|
||||||
fi
|
|
||||||
echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Prepare env file
|
|
||||||
run: |
|
|
||||||
cp apps/dokploy/.env.production.example .env.production
|
|
||||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
|
|
||||||
combine-manifests:
|
|
||||||
needs: [docker-amd, docker-arm]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create and push manifests
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
|
||||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
|
||||||
TAG="latest"
|
|
||||||
|
|
||||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
|
||||||
${IMAGE_NAME}:${TAG}-amd64 \
|
|
||||||
${IMAGE_NAME}:${TAG}-arm64
|
|
||||||
|
|
||||||
docker buildx imagetools create -t ${IMAGE_NAME}:${VERSION} \
|
|
||||||
${IMAGE_NAME}:${TAG}-amd64 \
|
|
||||||
${IMAGE_NAME}:${TAG}-arm64
|
|
||||||
|
|
||||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
|
||||||
TAG="canary"
|
|
||||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
|
||||||
${IMAGE_NAME}:${TAG}-amd64 \
|
|
||||||
${IMAGE_NAME}:${TAG}-arm64
|
|
||||||
|
|
||||||
else
|
|
||||||
TAG="feature"
|
|
||||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
|
||||||
${IMAGE_NAME}:${TAG}-amd64 \
|
|
||||||
${IMAGE_NAME}:${TAG}-arm64
|
|
||||||
fi
|
|
||||||
|
|
||||||
generate-release:
|
|
||||||
needs: [combine-manifests]
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.get_version.outputs.version }}
|
|
||||||
name: ${{ steps.get_version.outputs.version }}
|
|
||||||
generate_release_notes: true
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
118
.github/workflows/monitoring.yml
vendored
118
.github/workflows/monitoring.yml
vendored
@@ -1,118 +0,0 @@
|
|||||||
name: Dokploy Monitoring Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, canary]
|
|
||||||
|
|
||||||
env:
|
|
||||||
IMAGE_NAME: dokploy/monitoring
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker-amd:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set tag
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
|
||||||
TAG="latest"
|
|
||||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
|
||||||
TAG="canary"
|
|
||||||
else
|
|
||||||
TAG="feature"
|
|
||||||
fi
|
|
||||||
echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.monitoring
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
docker-arm:
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
|
||||||
TAG="latest"
|
|
||||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
|
||||||
TAG="canary"
|
|
||||||
else
|
|
||||||
TAG="feature"
|
|
||||||
fi
|
|
||||||
echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.monitoring
|
|
||||||
platforms: linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
|
|
||||||
combine-manifests:
|
|
||||||
needs: [docker-amd, docker-arm]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create and push manifests
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
|
||||||
TAG="latest"
|
|
||||||
|
|
||||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
|
||||||
${IMAGE_NAME}:${TAG}-amd64 \
|
|
||||||
${IMAGE_NAME}:${TAG}-arm64
|
|
||||||
|
|
||||||
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
|
|
||||||
TAG="canary"
|
|
||||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
|
||||||
${IMAGE_NAME}:${TAG}-amd64 \
|
|
||||||
${IMAGE_NAME}:${TAG}-arm64
|
|
||||||
|
|
||||||
else
|
|
||||||
TAG="feature"
|
|
||||||
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
|
|
||||||
${IMAGE_NAME}:${TAG}-amd64 \
|
|
||||||
${IMAGE_NAME}:${TAG}-arm64
|
|
||||||
fi
|
|
||||||
6
.github/workflows/pull-request.yml
vendored
6
.github/workflows/pull-request.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.9.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -34,11 +34,9 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
|
||||||
.db
|
|
||||||
@@ -14,10 +14,12 @@ We have a few guidelines to follow when contributing to this project:
|
|||||||
|
|
||||||
## Commit Convention
|
## 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 create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||||
|
|
||||||
### Commit Message Format
|
### Commit Message Format
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<type>[optional scope]: <description>
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
@@ -52,8 +54,6 @@ feat: add new feature
|
|||||||
|
|
||||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||||
|
|
||||||
We use Node v20.9.0
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dokploy/dokploy.git
|
git clone https://github.com/dokploy/dokploy.git
|
||||||
cd dokploy
|
cd dokploy
|
||||||
@@ -73,10 +73,9 @@ Run the command that will spin up all the required services and files.
|
|||||||
pnpm run dokploy:setup
|
pnpm run dokploy:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Run this script
|
Run this script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run server:script
|
pnpm run server:script
|
||||||
```
|
```
|
||||||
|
|
||||||
Now run the development server.
|
Now run the development server.
|
||||||
@@ -170,7 +169,6 @@ Let's take the example of `plausible` template.
|
|||||||
```typescript
|
```typescript
|
||||||
// EXAMPLE
|
// EXAMPLE
|
||||||
import {
|
import {
|
||||||
generateBase64,
|
|
||||||
generateHash,
|
generateHash,
|
||||||
generateRandomDomain,
|
generateRandomDomain,
|
||||||
type Template,
|
type Template,
|
||||||
@@ -202,8 +200,8 @@ export function generate(schema: Schema): Template {
|
|||||||
|
|
||||||
const mounts: Template["mounts"] = [
|
const mounts: Template["mounts"] = [
|
||||||
{
|
{
|
||||||
filePath: "./clickhouse/clickhouse-config.xml",
|
mountPath: "./clickhouse/clickhouse-config.xml",
|
||||||
content: "some content......",
|
content: `some content......`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -249,3 +247,4 @@ export function generate(schema: Schema): Template {
|
|||||||
## Docs & Website
|
## Docs & Website
|
||||||
|
|
||||||
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
|
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.9-slim AS base
|
FROM node:18-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy only the necessary files
|
# Copy only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
@@ -48,8 +48,6 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
|
|||||||
|
|
||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
|
|
||||||
ARG NIXPACKS_VERSION=1.29.1
|
|
||||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||||
&& chmod +x install.sh \
|
&& chmod +x install.sh \
|
||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.9-slim AS base
|
FROM node:18-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# Build stage
|
|
||||||
FROM golang:1.21-alpine3.19 AS builder
|
|
||||||
|
|
||||||
# Instalar dependencias necesarias
|
|
||||||
RUN apk add --no-cache gcc musl-dev sqlite-dev
|
|
||||||
|
|
||||||
# Establecer el directorio de trabajo
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copiar todo el código fuente primero
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Movernos al directorio de la aplicación golang
|
|
||||||
WORKDIR /app/apps/monitoring
|
|
||||||
|
|
||||||
# Descargar dependencias
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Compilar la aplicación
|
|
||||||
RUN CGO_ENABLED=1 GOOS=linux go build -o main main.go
|
|
||||||
|
|
||||||
# Etapa final
|
|
||||||
FROM alpine:3.19
|
|
||||||
|
|
||||||
# Instalar SQLite y otras dependencias necesarias
|
|
||||||
RUN apk add --no-cache sqlite-libs docker-cli
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copiar el binario compilado y el archivo monitor.go
|
|
||||||
COPY --from=builder /app/apps/monitoring/main ./main
|
|
||||||
COPY --from=builder /app/apps/monitoring/main.go ./monitor.go
|
|
||||||
|
|
||||||
# COPY --from=builder /app/apps/golang/.env ./.env
|
|
||||||
|
|
||||||
# Exponer el puerto
|
|
||||||
ENV PORT=3001
|
|
||||||
EXPOSE 3001
|
|
||||||
|
|
||||||
# Ejecutar la aplicación
|
|
||||||
CMD ["./main"]
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.9-slim AS base
|
FROM node:18-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.9-slim AS base
|
FROM node:18-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||||
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
|
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
|
||||||
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
@@ -92,8 +89,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
<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://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>
|
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
||||||
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
|
||||||
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||||
"build": "tsc --project tsconfig.json",
|
"build": "tsc --project tsconfig.json",
|
||||||
"start": "node dist/index.js",
|
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import "dotenv/config";
|
|||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { Queue } from "@nerimity/mimiqueue";
|
import { Queue } from "@nerimity/mimiqueue";
|
||||||
import { createClient } from "redis";
|
import { createClient } from "redis";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger";
|
||||||
import { type DeployJob, deployJobSchema } from "./schema.js";
|
import { type DeployJob, deployJobSchema } from "./schema";
|
||||||
import { deploy } from "./utils.js";
|
import { deploy } from "./utils";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const redisClient = createClient({
|
const redisClient = createClient({
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
20.9.0
|
18.18.0
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
describe("GitHub Webhook Skip CI", () => {
|
|
||||||
const mockGithubHeaders = {
|
|
||||||
"x-github-event": "push",
|
|
||||||
};
|
|
||||||
|
|
||||||
const createMockBody = (message: string) => ({
|
|
||||||
head_commit: {
|
|
||||||
message,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const skipKeywords = [
|
|
||||||
"[skip ci]",
|
|
||||||
"[ci skip]",
|
|
||||||
"[no ci]",
|
|
||||||
"[skip actions]",
|
|
||||||
"[actions skip]",
|
|
||||||
];
|
|
||||||
|
|
||||||
it("should detect skip keywords in commit message", () => {
|
|
||||||
for (const keyword of skipKeywords) {
|
|
||||||
const message = `feat: add new feature ${keyword}`;
|
|
||||||
const commitMessage = extractCommitMessage(
|
|
||||||
mockGithubHeaders,
|
|
||||||
createMockBody(message),
|
|
||||||
);
|
|
||||||
expect(commitMessage.includes(keyword)).toBe(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not detect skip keywords in normal commit message", () => {
|
|
||||||
const message = "feat: add new feature";
|
|
||||||
const commitMessage = extractCommitMessage(
|
|
||||||
mockGithubHeaders,
|
|
||||||
createMockBody(message),
|
|
||||||
);
|
|
||||||
for (const keyword of skipKeywords) {
|
|
||||||
expect(commitMessage.includes(keyword)).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle different webhook sources", () => {
|
|
||||||
// GitHub
|
|
||||||
expect(
|
|
||||||
extractCommitMessage(
|
|
||||||
{ "x-github-event": "push" },
|
|
||||||
{ head_commit: { message: "[skip ci] test" } },
|
|
||||||
),
|
|
||||||
).toBe("[skip ci] test");
|
|
||||||
|
|
||||||
// GitLab
|
|
||||||
expect(
|
|
||||||
extractCommitMessage(
|
|
||||||
{ "x-gitlab-event": "push" },
|
|
||||||
{ commits: [{ message: "[skip ci] test" }] },
|
|
||||||
),
|
|
||||||
).toBe("[skip ci] test");
|
|
||||||
|
|
||||||
// Bitbucket
|
|
||||||
expect(
|
|
||||||
extractCommitMessage(
|
|
||||||
{ "x-event-key": "repo:push" },
|
|
||||||
{
|
|
||||||
push: {
|
|
||||||
changes: [{ new: { target: { message: "[skip ci] test" } } }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe("[skip ci] test");
|
|
||||||
|
|
||||||
// Gitea
|
|
||||||
expect(
|
|
||||||
extractCommitMessage(
|
|
||||||
{ "x-gitea-event": "push" },
|
|
||||||
{ commits: [{ message: "[skip ci] test" }] },
|
|
||||||
),
|
|
||||||
).toBe("[skip ci] test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle missing commit message", () => {
|
|
||||||
expect(extractCommitMessage(mockGithubHeaders, {})).toBe("NEW COMMIT");
|
|
||||||
expect(extractCommitMessage({ "x-gitlab-event": "push" }, {})).toBe(
|
|
||||||
"NEW COMMIT",
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
extractCommitMessage(
|
|
||||||
{ "x-event-key": "repo:push" },
|
|
||||||
{ push: { changes: [] } },
|
|
||||||
),
|
|
||||||
).toBe("NEW COMMIT");
|
|
||||||
expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe(
|
|
||||||
"NEW COMMIT",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -14,32 +14,6 @@ import {
|
|||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: Admin = {
|
const baseAdmin: Admin = {
|
||||||
enablePaidFeatures: false,
|
|
||||||
metricsConfig: {
|
|
||||||
containers: {
|
|
||||||
refreshRate: 20,
|
|
||||||
services: {
|
|
||||||
include: [],
|
|
||||||
exclude: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
type: "Dokploy",
|
|
||||||
cronJob: "",
|
|
||||||
port: 4500,
|
|
||||||
refreshRate: 20,
|
|
||||||
retentionDays: 2,
|
|
||||||
token: "",
|
|
||||||
thresholds: {
|
|
||||||
cpu: 0,
|
|
||||||
memory: 0,
|
|
||||||
},
|
|
||||||
urlCallback: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cleanupCacheApplications: false,
|
|
||||||
cleanupCacheOnCompose: false,
|
|
||||||
cleanupCacheOnPreviews: false,
|
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
authId: "",
|
authId: "",
|
||||||
adminId: "string",
|
adminId: "string",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default defineConfig({
|
|||||||
NODE: "test",
|
NODE: "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [tsconfigPaths()],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@dokploy/server": path.resolve(
|
"@dokploy/server": path.resolve(
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ import { CardTitle } from "@/components/ui/card";
|
|||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSeparator,
|
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/components/ui/input-otp";
|
} from "@/components/ui/input-otp";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -89,31 +87,25 @@ export const Login2FA = ({ authId }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CardTitle className="text-xl font-bold">2FA Login</CardTitle>
|
<CardTitle className="text-xl font-bold">2FA Setup</CardTitle>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="pin"
|
name="pin"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col max-sm:items-center">
|
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
||||||
<FormLabel>Pin</FormLabel>
|
<FormLabel>Pin</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex">
|
<InputOTP maxLength={6} {...field}>
|
||||||
<InputOTP
|
<InputOTPGroup>
|
||||||
maxLength={6}
|
<InputOTPSlot index={0} />
|
||||||
{...field}
|
<InputOTPSlot index={1} />
|
||||||
pattern={REGEXP_ONLY_DIGITS}
|
<InputOTPSlot index={2} />
|
||||||
>
|
<InputOTPSlot index={3} />
|
||||||
<InputOTPGroup>
|
<InputOTPSlot index={4} />
|
||||||
<InputOTPSlot index={0} className="border-border" />
|
<InputOTPSlot index={5} />
|
||||||
<InputOTPSlot index={1} className="border-border" />
|
</InputOTPGroup>
|
||||||
<InputOTPSlot index={2} className="border-border" />
|
</InputOTP>
|
||||||
<InputOTPSlot index={3} className="border-border" />
|
|
||||||
<InputOTPSlot index={4} className="border-border" />
|
|
||||||
<InputOTPSlot index={5} className="border-border" />
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Please enter the 6 digits code provided by your authenticator
|
Please enter the 6 digits code provided by your authenticator
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the swarm settings");
|
toast.error("Error to update the swarm settings");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the command");
|
toast.error("Error to update the command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const AddCommand = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the command");
|
toast.error("Error to update the command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,8 +81,7 @@ export const AddCommand = ({ applicationId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Run Command</CardTitle>
|
<CardTitle className="text-xl">Run Command</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Run a custom command in the container after the application
|
Run a custom command in the container
|
||||||
initialized
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -45,29 +45,18 @@ type AddPort = z.infer<typeof AddPortSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
portId?: string;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandlePorts = ({
|
export const AddPort = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
portId,
|
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { data } = api.port.one.useQuery(
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
{
|
api.port.create.useMutation();
|
||||||
portId: portId ?? "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!portId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { mutateAsync, isLoading, error, isError } = portId
|
|
||||||
? api.port.update.useMutation()
|
|
||||||
: api.port.create.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddPort>({
|
const form = useForm<AddPort>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -79,46 +68,32 @@ export const HandlePorts = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
publishedPort: data?.publishedPort ?? 0,
|
publishedPort: 0,
|
||||||
targetPort: data?.targetPort ?? 0,
|
targetPort: 0,
|
||||||
protocol: data?.protocol ?? "tcp",
|
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddPort) => {
|
const onSubmit = async (data: AddPort) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
portId: portId || "",
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success(portId ? "Port Updated" : "Port Created");
|
toast.success("Port Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error("Error to create the port");
|
||||||
portId ? "Error updating the port" : "Error creating the port",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{portId ? (
|
<Button>{children}</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10 "
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button>{children}</Button>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -229,7 +204,7 @@ export const HandlePorts = ({
|
|||||||
form="hook-form-add-port"
|
form="hook-form-add-port"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{portId ? "Update" : "Create"}
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
portId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeletePort = ({ portId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { mutateAsync, isLoading } = api.port.delete.useMutation();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the port
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
portId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
utils.application.one.invalidate({
|
||||||
|
applicationId: data?.applicationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Port delete succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to delete the port");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -9,25 +7,23 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Rss, Trash2 } from "lucide-react";
|
import { Rss } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { AddPort } from "./add-port";
|
||||||
import { HandlePorts } from "./handle-ports";
|
import { DeletePort } from "./delete-port";
|
||||||
|
import { UpdatePort } from "./update-port";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPorts = ({ applicationId }: Props) => {
|
export const ShowPorts = ({ applicationId }: Props) => {
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: deletePort, isLoading: isRemoving } =
|
|
||||||
api.port.delete.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -39,7 +35,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.ports.length > 0 && (
|
{data && data?.ports.length > 0 && (
|
||||||
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -49,7 +45,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No ports configured
|
No ports configured
|
||||||
</span>
|
</span>
|
||||||
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
@@ -82,36 +78,8 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<HandlePorts
|
<UpdatePort portId={port.portId} />
|
||||||
applicationId={applicationId}
|
<DeletePort portId={port.portId} />
|
||||||
portId={port.portId}
|
|
||||||
/>
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Port"
|
|
||||||
description="Are you sure you want to delete this port?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deletePort({
|
|
||||||
portId: port.portId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
toast.success("Port deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting port");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10 "
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const UpdatePortSchema = z.object({
|
||||||
|
publishedPort: z.number().int().min(1).max(65535),
|
||||||
|
targetPort: z.number().int().min(1).max(65535),
|
||||||
|
protocol: z.enum(["tcp", "udp"], {
|
||||||
|
required_error: "Protocol is required",
|
||||||
|
invalid_type_error: "Protocol must be a valid protocol",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type UpdatePort = z.infer<typeof UpdatePortSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
portId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdatePort = ({ portId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data } = api.port.one.useQuery(
|
||||||
|
{
|
||||||
|
portId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!portId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.port.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<UpdatePort>({
|
||||||
|
defaultValues: {},
|
||||||
|
resolver: zodResolver(UpdatePortSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
publishedPort: data.publishedPort,
|
||||||
|
targetPort: data.targetPort,
|
||||||
|
protocol: data.protocol,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: UpdatePort) => {
|
||||||
|
await mutateAsync({
|
||||||
|
portId,
|
||||||
|
publishedPort: data.publishedPort,
|
||||||
|
targetPort: data.targetPort,
|
||||||
|
protocol: data.protocol,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
toast.success("Port Updated");
|
||||||
|
await utils.application.one.invalidate({
|
||||||
|
applicationId: response?.applicationId,
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the port");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update</DialogTitle>
|
||||||
|
<DialogDescription>Update the port</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-update-redirect"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="publishedPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Published Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder="1-65535" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="targetPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Target Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder="1-65535" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="protocol"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<FormLabel>Protocol</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a protocol" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent defaultValue={"none"}>
|
||||||
|
<SelectItem value={"none"} disabled>
|
||||||
|
None
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={"tcp"}>TCP</SelectItem>
|
||||||
|
<SelectItem value={"udp"}>UDP</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-update-redirect"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -31,7 +31,7 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -77,32 +77,19 @@ const redirectPresets = [
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
redirectId?: string;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandleRedirect = ({
|
export const AddRedirect = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
redirectId,
|
|
||||||
children = <PlusIcon className="w-4 h-4" />,
|
children = <PlusIcon className="w-4 h-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [presetSelected, setPresetSelected] = useState("");
|
const [presetSelected, setPresetSelected] = useState("");
|
||||||
|
|
||||||
const { data, refetch } = api.redirects.one.useQuery(
|
|
||||||
{
|
|
||||||
redirectId: redirectId || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!redirectId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } = redirectId
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
? api.redirects.update.useMutation()
|
api.redirects.create.useMutation();
|
||||||
: api.redirects.create.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddRedirect>({
|
const form = useForm<AddRedirect>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -115,35 +102,29 @@ export const HandleRedirect = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
permanent: data?.permanent || false,
|
permanent: false,
|
||||||
regex: data?.regex || "",
|
regex: "",
|
||||||
replacement: data?.replacement || "",
|
replacement: "",
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddRedirect) => {
|
const onSubmit = async (data: AddRedirect) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
redirectId: redirectId || "",
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success(redirectId ? "Redirect Updated" : "Redirect Created");
|
toast.success("Redirect Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
refetch();
|
|
||||||
await utils.application.readTraefikConfig.invalidate({
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
onDialogToggle(false);
|
onDialogToggle(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error("Error to create the redirect");
|
||||||
redirectId
|
|
||||||
? "Error updating the redirect"
|
|
||||||
: "Error creating the redirect",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,17 +148,7 @@ export const HandleRedirect = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{redirectId ? (
|
<Button>{children}</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10 "
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button>{children}</Button>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -272,7 +243,7 @@ export const HandleRedirect = ({
|
|||||||
form="hook-form-add-redirect"
|
form="hook-form-add-redirect"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{redirectId ? "Update" : "Create"}
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
redirectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteRedirect = ({ redirectId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { mutateAsync, isLoading } = api.redirects.delete.useMutation();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the
|
||||||
|
redirect
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
redirectId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
utils.application.one.invalidate({
|
||||||
|
applicationId: data?.applicationId,
|
||||||
|
});
|
||||||
|
utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId: data?.applicationId,
|
||||||
|
});
|
||||||
|
toast.success("Redirect delete succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to delete the redirect");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -8,28 +6,23 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Split, Trash2 } from "lucide-react";
|
import { Split } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { AddRedirect } from "./add-redirect";
|
||||||
import { HandleRedirect } from "./handle-redirect";
|
import { DeleteRedirect } from "./delete-redirect";
|
||||||
|
import { UpdateRedirect } from "./update-redirect";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowRedirects = ({ applicationId }: Props) => {
|
export const ShowRedirects = ({ applicationId }: Props) => {
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
|
|
||||||
api.redirects.delete.useMutation();
|
|
||||||
|
|
||||||
const utils = api.useUtils();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -42,9 +35,7 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.redirects.length > 0 && (
|
{data && data?.redirects.length > 0 && (
|
||||||
<HandleRedirect applicationId={applicationId}>
|
<AddRedirect applicationId={applicationId}>Add Redirect</AddRedirect>
|
||||||
Add Redirect
|
|
||||||
</HandleRedirect>
|
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -54,9 +45,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No redirects configured
|
No redirects configured
|
||||||
</span>
|
</span>
|
||||||
<HandleRedirect applicationId={applicationId}>
|
<AddRedirect applicationId={applicationId}>
|
||||||
Add Redirect
|
Add Redirect
|
||||||
</HandleRedirect>
|
</AddRedirect>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -85,40 +76,8 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<HandleRedirect
|
<UpdateRedirect redirectId={redirect.redirectId} />
|
||||||
redirectId={redirect.redirectId}
|
<DeleteRedirect redirectId={redirect.redirectId} />
|
||||||
applicationId={applicationId}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Redirect"
|
|
||||||
description="Are you sure you want to delete this redirect?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteRedirect({
|
|
||||||
redirectId: redirect.redirectId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Redirect deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting redirect");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
const UpdateRedirectSchema = z.object({
|
||||||
|
regex: z.string().min(1, "Regex required"),
|
||||||
|
permanent: z.boolean().default(false),
|
||||||
|
replacement: z.string().min(1, "Replacement required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type UpdateRedirect = z.infer<typeof UpdateRedirectSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
redirectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data } = api.redirects.one.useQuery(
|
||||||
|
{
|
||||||
|
redirectId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!redirectId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.redirects.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<UpdateRedirect>({
|
||||||
|
defaultValues: {
|
||||||
|
permanent: false,
|
||||||
|
regex: "",
|
||||||
|
replacement: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(UpdateRedirectSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
permanent: data.permanent || false,
|
||||||
|
regex: data.regex || "",
|
||||||
|
replacement: data.replacement || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: UpdateRedirect) => {
|
||||||
|
await mutateAsync({
|
||||||
|
redirectId,
|
||||||
|
permanent: data.permanent,
|
||||||
|
regex: data.regex,
|
||||||
|
replacement: data.replacement,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
toast.success("Redirect Updated");
|
||||||
|
await utils.application.one.invalidate({
|
||||||
|
applicationId: response?.applicationId,
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the redirect");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update</DialogTitle>
|
||||||
|
<DialogDescription>Update the redirect</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-update-redirect"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="regex"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Regex</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="^http://localhost/(.*)" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="replacement"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Replacement</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="http://mydomain/$${1}" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="permanent"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Permanent</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Set the permanent option to true to apply a permanent
|
||||||
|
redirection.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-update-redirect"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -35,29 +35,17 @@ type AddSecurity = z.infer<typeof AddSecuritychema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
securityId?: string;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandleSecurity = ({
|
export const AddSecurity = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
securityId,
|
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data } = api.security.one.useQuery(
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
{
|
api.security.create.useMutation();
|
||||||
securityId: securityId ?? "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!securityId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } = securityId
|
|
||||||
? api.security.update.useMutation()
|
|
||||||
: api.security.create.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddSecurity>({
|
const form = useForm<AddSecurity>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -68,20 +56,16 @@ export const HandleSecurity = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset();
|
||||||
username: data?.username || "",
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
password: data?.password || "",
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddSecurity) => {
|
const onSubmit = async (data: AddSecurity) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
securityId: securityId || "",
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success(securityId ? "Security Updated" : "Security Created");
|
toast.success("Security Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
@@ -91,34 +75,20 @@ export const HandleSecurity = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error("Error to create the security");
|
||||||
securityId
|
|
||||||
? "Error updating the security"
|
|
||||||
: "Error creating security",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{securityId ? (
|
<Button>{children}</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10 "
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button>{children}</Button>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Security</DialogTitle>
|
<DialogTitle>Security</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{securityId ? "Update" : "Add"} security to your application
|
Add security to your application
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
@@ -167,7 +137,7 @@ export const HandleSecurity = ({
|
|||||||
form="hook-form-add-security"
|
form="hook-form-add-security"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{securityId ? "Update" : "Create"}
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
securityId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteSecurity = ({ securityId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { mutateAsync, isLoading } = api.security.delete.useMutation();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the
|
||||||
|
security
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
securityId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
utils.application.one.invalidate({
|
||||||
|
applicationId: data?.applicationId,
|
||||||
|
});
|
||||||
|
utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId: data?.applicationId,
|
||||||
|
});
|
||||||
|
toast.success("Security delete succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to delete the security");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -8,27 +6,23 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { LockKeyhole, Trash2 } from "lucide-react";
|
import { LockKeyhole } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { AddSecurity } from "./add-security";
|
||||||
import { HandleSecurity } from "./handle-security";
|
import { DeleteSecurity } from "./delete-security";
|
||||||
|
import { UpdateSecurity } from "./update-security";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowSecurity = ({ applicationId }: Props) => {
|
export const ShowSecurity = ({ applicationId }: Props) => {
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
|
|
||||||
api.security.delete.useMutation();
|
|
||||||
|
|
||||||
const utils = api.useUtils();
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -38,9 +32,7 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.security.length > 0 && (
|
{data && data?.security.length > 0 && (
|
||||||
<HandleSecurity applicationId={applicationId}>
|
<AddSecurity applicationId={applicationId}>Add Security</AddSecurity>
|
||||||
Add Security
|
|
||||||
</HandleSecurity>
|
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -50,9 +42,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No security configured
|
No security configured
|
||||||
</span>
|
</span>
|
||||||
<HandleSecurity applicationId={applicationId}>
|
<AddSecurity applicationId={applicationId}>
|
||||||
Add Security
|
Add Security
|
||||||
</HandleSecurity>
|
</AddSecurity>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -75,39 +67,8 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<HandleSecurity
|
<UpdateSecurity securityId={security.securityId} />
|
||||||
securityId={security.securityId}
|
<DeleteSecurity securityId={security.securityId} />
|
||||||
applicationId={applicationId}
|
|
||||||
/>
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Security"
|
|
||||||
description="Are you sure you want to delete this security?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteSecurity({
|
|
||||||
securityId: security.securityId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Security deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting security");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const UpdateSecuritySchema = z.object({
|
||||||
|
username: z.string().min(1, "Username is required"),
|
||||||
|
password: z.string().min(1, "Password is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type UpdateSecurity = z.infer<typeof UpdateSecuritySchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
securityId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateSecurity = ({ securityId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data } = api.security.one.useQuery(
|
||||||
|
{
|
||||||
|
securityId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!securityId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.security.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<UpdateSecurity>({
|
||||||
|
defaultValues: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(UpdateSecuritySchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
username: data.username || "",
|
||||||
|
password: data.password || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: UpdateSecurity) => {
|
||||||
|
await mutateAsync({
|
||||||
|
securityId,
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
toast.success("Security Updated");
|
||||||
|
await utils.application.one.invalidate({
|
||||||
|
applicationId: response?.applicationId,
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the security");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update</DialogTitle>
|
||||||
|
<DialogDescription>Update the security</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-update-security"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4 "
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="test1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="test" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-update-security"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const addResourcesApplication = z.object({
|
||||||
|
memoryReservation: z.number().nullable().optional(),
|
||||||
|
cpuLimit: z.number().nullable().optional(),
|
||||||
|
memoryLimit: z.number().nullable().optional(),
|
||||||
|
cpuReservation: z.number().nullable().optional(),
|
||||||
|
});
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddResourcesApplication = z.infer<typeof addResourcesApplication>;
|
||||||
|
|
||||||
|
export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||||
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
|
{
|
||||||
|
applicationId,
|
||||||
|
},
|
||||||
|
{ enabled: !!applicationId },
|
||||||
|
);
|
||||||
|
const { mutateAsync, isLoading } = api.application.update.useMutation();
|
||||||
|
const form = useForm<AddResourcesApplication>({
|
||||||
|
defaultValues: {},
|
||||||
|
resolver: zodResolver(addResourcesApplication),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
cpuLimit: data?.cpuLimit || undefined,
|
||||||
|
cpuReservation: data?.cpuReservation || undefined,
|
||||||
|
memoryLimit: data?.memoryLimit || undefined,
|
||||||
|
memoryReservation: data?.memoryReservation || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: AddResourcesApplication) => {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId,
|
||||||
|
cpuLimit: formData.cpuLimit || null,
|
||||||
|
cpuReservation: formData.cpuReservation || null,
|
||||||
|
memoryLimit: formData.memoryLimit || null,
|
||||||
|
memoryReservation: formData.memoryReservation || null,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Resources Updated");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to Update the resources");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
If you want to decrease or increase the resources to a specific.
|
||||||
|
application or database
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<AlertBlock type="info">
|
||||||
|
Please remember to click Redeploy after modify the resources to apply
|
||||||
|
the changes.
|
||||||
|
</AlertBlock>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<div className="grid w-full md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="memoryReservation"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Memory Reservation</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="256 MB"
|
||||||
|
{...field}
|
||||||
|
value={field.value?.toString() || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
field.onChange(null);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
field.onChange(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="memoryLimit"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Memory Limit</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={"1024 MB"}
|
||||||
|
{...field}
|
||||||
|
value={field.value?.toString() || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
field.onChange(null);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
field.onChange(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cpuLimit"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Cpu Limit</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={"2"}
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
value={field.value?.toString() || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (
|
||||||
|
value === "" ||
|
||||||
|
/^[0-9]*\.?[0-9]*$/.test(value)
|
||||||
|
) {
|
||||||
|
const float = Number.parseFloat(value);
|
||||||
|
field.onChange(float);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cpuReservation"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Cpu Reservation</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={"1"}
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
value={field.value?.toString() || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (
|
||||||
|
value === "" ||
|
||||||
|
/^[0-9]*\.?[0-9]*$/.test(value)
|
||||||
|
) {
|
||||||
|
const float = Number.parseFloat(value);
|
||||||
|
field.onChange(float);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { InfoIcon } from "lucide-react";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const addResourcesSchema = z.object({
|
|
||||||
memoryReservation: z.string().optional(),
|
|
||||||
cpuLimit: z.string().optional(),
|
|
||||||
memoryLimit: z.string().optional(),
|
|
||||||
cpuReservation: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ServiceType =
|
|
||||||
| "postgres"
|
|
||||||
| "mongo"
|
|
||||||
| "redis"
|
|
||||||
| "mysql"
|
|
||||||
| "mariadb"
|
|
||||||
| "application";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id: string;
|
|
||||||
type: ServiceType | "application";
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddResources = z.infer<typeof addResourcesSchema>;
|
|
||||||
export const ShowResources = ({ id, type }: Props) => {
|
|
||||||
const queryMap = {
|
|
||||||
postgres: () =>
|
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
|
||||||
mariadb: () =>
|
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
|
||||||
application: () =>
|
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
|
||||||
};
|
|
||||||
const { data, refetch } = queryMap[type]
|
|
||||||
? queryMap[type]()
|
|
||||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
|
||||||
|
|
||||||
const mutationMap = {
|
|
||||||
postgres: () => api.postgres.update.useMutation(),
|
|
||||||
redis: () => api.redis.update.useMutation(),
|
|
||||||
mysql: () => api.mysql.update.useMutation(),
|
|
||||||
mariadb: () => api.mariadb.update.useMutation(),
|
|
||||||
application: () => api.application.update.useMutation(),
|
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = mutationMap[type]
|
|
||||||
? mutationMap[type]()
|
|
||||||
: api.mongo.update.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddResources>({
|
|
||||||
defaultValues: {
|
|
||||||
cpuLimit: "",
|
|
||||||
cpuReservation: "",
|
|
||||||
memoryLimit: "",
|
|
||||||
memoryReservation: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(addResourcesSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
cpuLimit: data?.cpuLimit || undefined,
|
|
||||||
cpuReservation: data?.cpuReservation || undefined,
|
|
||||||
memoryLimit: data?.memoryLimit || undefined,
|
|
||||||
memoryReservation: data?.memoryReservation || undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data, form, form.reset]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: AddResources) => {
|
|
||||||
await mutateAsync({
|
|
||||||
mongoId: id || "",
|
|
||||||
postgresId: id || "",
|
|
||||||
redisId: id || "",
|
|
||||||
mysqlId: id || "",
|
|
||||||
mariadbId: id || "",
|
|
||||||
applicationId: id || "",
|
|
||||||
cpuLimit: formData.cpuLimit || null,
|
|
||||||
cpuReservation: formData.cpuReservation || null,
|
|
||||||
memoryLimit: formData.memoryLimit || null,
|
|
||||||
memoryReservation: formData.memoryReservation || null,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Resources Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the resources");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
If you want to decrease or increase the resources to a specific.
|
|
||||||
application or database
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<AlertBlock type="info">
|
|
||||||
Please remember to click Redeploy after modify the resources to apply
|
|
||||||
the changes.
|
|
||||||
</AlertBlock>
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-8 "
|
|
||||||
>
|
|
||||||
<div className="grid w-full md:grid-cols-2 gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="memoryLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory hard limit in bytes. Example: 1GB =
|
|
||||||
1073741824 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1073741824 (1GB in bytes)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="memoryReservation"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory soft limit in bytes. Example: 256MB =
|
|
||||||
268435456 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="268435456 (256MB in bytes)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
|
||||||
CPUs = 2000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="2000000000 (2 CPUs)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuReservation"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU shares (relative weight). Example: 1 CPU =
|
|
||||||
1000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="1000000000 (1 CPU)" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-end">
|
|
||||||
<Button isLoading={isLoading} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -105,7 +105,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
form.reset();
|
form.reset();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the Traefik config");
|
toast.error("Error to update the traefik config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error creating the Bind mount");
|
toast.error("Error to create the Bind mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "volume") {
|
} else if (data.type === "volume") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -122,7 +122,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error creating the Volume mount");
|
toast.error("Error to create the Volume mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "file") {
|
} else if (data.type === "file") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -138,7 +138,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error creating the File mount");
|
toast.error("Error to create the File mount");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mountId: string;
|
||||||
|
refetch: () => void;
|
||||||
|
}
|
||||||
|
export const DeleteVolume = ({ mountId, refetch }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.mounts.remove.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the mount
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
mountId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Mount deleted succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to delete the mount");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -9,49 +7,40 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Package, Trash2 } from "lucide-react";
|
import { Package } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import type { ServiceType } from "../show-resources";
|
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
|
import { DeleteVolume } from "./delete-volume";
|
||||||
import { UpdateVolume } from "./update-volume";
|
import { UpdateVolume } from "./update-volume";
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: ServiceType | "compose";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowVolumes = ({ id, type }: Props) => {
|
export const ShowVolumes = ({ applicationId }: Props) => {
|
||||||
const queryMap = {
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
postgres: () =>
|
{
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
applicationId,
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
},
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
{ enabled: !!applicationId },
|
||||||
mariadb: () =>
|
);
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
|
||||||
application: () =>
|
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
|
||||||
compose: () =>
|
|
||||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
|
||||||
};
|
|
||||||
const { data, refetch } = queryMap[type]
|
|
||||||
? queryMap[type]()
|
|
||||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
|
||||||
const { mutateAsync: deleteVolume, isLoading: isRemoving } =
|
|
||||||
api.mounts.remove.useMutation();
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Volumes</CardTitle>
|
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to persist data in this service use the following config
|
If you want to persist data in this application use the following
|
||||||
to setup the volumes
|
config to setup the volumes
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
{data && data?.mounts.length > 0 && (
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
<AddVolumes
|
||||||
|
serviceId={applicationId}
|
||||||
|
refetch={refetch}
|
||||||
|
serviceType="application"
|
||||||
|
>
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
)}
|
)}
|
||||||
@@ -63,13 +52,17 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No volumes/mounts configured
|
No volumes/mounts configured
|
||||||
</span>
|
</span>
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
<AddVolumes
|
||||||
|
serviceId={applicationId}
|
||||||
|
refetch={refetch}
|
||||||
|
serviceType="application"
|
||||||
|
>
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="info">
|
||||||
Please remember to click Redeploy after adding, editing, or
|
Please remember to click Redeploy after adding, editing, or
|
||||||
deleting a mount to apply the changes.
|
deleting a mount to apply the changes.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
@@ -80,8 +73,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
key={mount.mountId}
|
key={mount.mountId}
|
||||||
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
||||||
>
|
>
|
||||||
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Mount Type</span>
|
<span className="font-medium">Mount Type</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -98,12 +90,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<div className="flex flex-col gap-1">
|
<>
|
||||||
<span className="font-medium">Content</span>
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
|
<span className="font-medium">Content</span>
|
||||||
{mount.content}
|
<span className="text-sm text-muted-foreground">
|
||||||
</span>
|
{mount.content}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">File Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.filePath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{mount.type === "bind" && (
|
{mount.type === "bind" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -113,55 +114,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mount.type === "file" ? (
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex flex-col gap-1">
|
<span className="font-medium">Mount Path</span>
|
||||||
<span className="font-medium">File Path</span>
|
<span className="text-sm text-muted-foreground">
|
||||||
<span className="text-sm text-muted-foreground">
|
{mount.mountPath}
|
||||||
{mount.filePath}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="font-medium">Mount Path</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{mount.mountPath}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateVolume
|
<UpdateVolume
|
||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
serviceType={type}
|
serviceType="application"
|
||||||
/>
|
/>
|
||||||
<DialogAction
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
title="Delete Volume"
|
|
||||||
description="Are you sure you want to delete this volume?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteVolume({
|
|
||||||
mountId: mount.mountId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
toast.success("Volume deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting volume");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
import { Pencil } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -139,7 +139,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the Bind mount");
|
toast.error("Error to update the Bind mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "volume") {
|
} else if (data.type === "volume") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -153,7 +153,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the Volume mount");
|
toast.error("Error to update the Volume mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "file") {
|
} else if (data.type === "file") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -168,7 +168,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the File mount");
|
toast.error("Error to update the File mount");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
refetch();
|
refetch();
|
||||||
@@ -177,13 +177,8 @@ export const UpdateVolume = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
variant="ghost"
|
<Pencil className="size-4 text-muted-foreground" />
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10 "
|
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the build type");
|
toast.error("Error to save the build type");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,12 +20,6 @@ interface Props {
|
|||||||
|
|
||||||
export const CancelQueues = ({ applicationId }: Props) => {
|
export const CancelQueues = ({ applicationId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
|
|
||||||
if (isCloud) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const RefreshToken = ({ applicationId }: Props) => {
|
|||||||
toast.success("Refresh updated");
|
toast.success("Refresh updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the refresh token");
|
toast.error("Error to update the refresh token");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -7,45 +5,18 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
errorMessage?: string;
|
|
||||||
}
|
}
|
||||||
export const ShowDeployment = ({
|
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||||
logPath,
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
serverId,
|
|
||||||
errorMessage,
|
|
||||||
}: Props) => {
|
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
if (autoScroll && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!scrollRef.current) return;
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
||||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
|
||||||
setAutoScroll(isAtBottom);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
@@ -77,36 +48,13 @@ export const ShowDeployment = ({
|
|||||||
};
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
useEffect(() => {
|
const scrollToBottom = () => {
|
||||||
const logs = parseLogs(data);
|
endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
let filteredLogsResult = logs;
|
};
|
||||||
if (serverId) {
|
|
||||||
let hideSubsequentLogs = false;
|
|
||||||
filteredLogsResult = logs.filter((log) => {
|
|
||||||
if (
|
|
||||||
log.message.includes(
|
|
||||||
"===================================EXTRA LOGS============================================",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
hideSubsequentLogs = true;
|
|
||||||
return showExtraLogs;
|
|
||||||
}
|
|
||||||
return showExtraLogs ? true : !hideSubsequentLogs;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilteredLogs(filteredLogsResult);
|
|
||||||
}, [data, showExtraLogs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
}, [data]);
|
||||||
if (autoScroll && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [filteredLogs, autoScroll]);
|
|
||||||
|
|
||||||
const optionalErrors = parseLogs(errorMessage || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -127,57 +75,18 @@ export const ShowDeployment = ({
|
|||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription className="flex items-center gap-2">
|
<DialogDescription>
|
||||||
<span>
|
See all the details of this deployment
|
||||||
See all the details of this deployment |{" "}
|
|
||||||
<Badge variant="blank" className="text-xs">
|
|
||||||
{filteredLogs.length} lines
|
|
||||||
</Badge>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{serverId && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="show-extra-logs"
|
|
||||||
checked={showExtraLogs}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setShowExtraLogs(checked as boolean)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="show-extra-logs"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
Show Extra Logs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div
|
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem]">
|
||||||
ref={scrollRef}
|
<code>
|
||||||
onScroll={handleScroll}
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
{data || "Loading..."}
|
||||||
>
|
</pre>
|
||||||
{" "}
|
<div ref={endOfLogsRef} />
|
||||||
{filteredLogs.length > 0 ? (
|
</code>
|
||||||
filteredLogs.map((log: LogLine, index: number) => (
|
|
||||||
<TerminalLine key={index} log={log} noTimestamp />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{optionalErrors.length > 0 ? (
|
|
||||||
optionalErrors.map((log: LogLine, index: number) => (
|
|
||||||
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RocketIcon } from "lucide-react";
|
import { RocketIcon } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { CancelQueues } from "./cancel-queues";
|
import { CancelQueues } from "./cancel-queues";
|
||||||
@@ -18,11 +18,8 @@ import { ShowDeployment } from "./show-deployment";
|
|||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDeployments = ({ applicationId }: Props) => {
|
export const ShowDeployments = ({ applicationId }: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
|
||||||
>(null);
|
|
||||||
const { data } = api.application.one.useQuery({ applicationId });
|
const { data } = api.application.one.useQuery({ applicationId });
|
||||||
const { data: deployments } = api.deployment.all.useQuery(
|
const { data: deployments } = api.deployment.all.useQuery(
|
||||||
{ applicationId },
|
{ applicationId },
|
||||||
@@ -103,7 +100,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveLog(deployment);
|
setActiveLog(deployment.logPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@@ -115,10 +112,9 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
open={activeLog !== null}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog?.logPath || ""}
|
logPath={activeLog}
|
||||||
errorMessage={activeLog?.errorMessage || ""}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ export const AddDomain = ({
|
|||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
error: domainId
|
||||||
|
? "Error to update the domain"
|
||||||
|
: "Error to create the domain",
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
@@ -262,21 +264,21 @@ export const AddDomain = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
<FormLabel>Certificate</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate provider" />
|
<SelectValue placeholder="Select a certificate" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Let's Encrypt
|
Letsencrypt (Default)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
domainId: string;
|
||||||
|
}
|
||||||
|
export const DeleteDomain = ({ domainId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.domain.delete.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<TrashIcon className="size-4 text-muted-foreground " />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the
|
||||||
|
domain
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
domainId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data?.applicationId) {
|
||||||
|
utils.domain.byApplicationId.invalidate({
|
||||||
|
applicationId: data?.applicationId,
|
||||||
|
});
|
||||||
|
utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId: data?.applicationId,
|
||||||
|
});
|
||||||
|
} else if (data?.composeId) {
|
||||||
|
utils.domain.byComposeId.invalidate({
|
||||||
|
composeId: data?.composeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Domain delete succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to delete Domain");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -7,18 +6,19 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
|
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { AddDomain } from "./add-domain";
|
import { AddDomain } from "./add-domain";
|
||||||
|
import { DeleteDomain } from "./delete-domain";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ applicationId }: Props) => {
|
export const ShowDomains = ({ applicationId }: Props) => {
|
||||||
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
const { data } = api.domain.byApplicationId.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
@@ -26,10 +26,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
|
||||||
api.domain.delete.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -73,66 +69,35 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.domainId}
|
key={item.domainId}
|
||||||
className="flex w-full items-center justify-between gap-4 border p-4 md:px-6 rounded-lg flex-wrap"
|
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
className="md:basis-1/2 flex gap-2 items-center hover:underline transition-all w-full"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||||
>
|
>
|
||||||
<span className="truncate max-w-full text-sm">
|
<ExternalLink className="size-5" />
|
||||||
{item.host}
|
|
||||||
</span>
|
|
||||||
<ExternalLink className="size-4 min-w-4" />
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex gap-8">
|
<Input disabled value={item.host} />
|
||||||
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
|
<Button variant="outline" disabled>
|
||||||
<span>{item.path}</span>
|
{item.path}
|
||||||
<span>{item.port}</span>
|
</Button>
|
||||||
<span>{item.https ? "HTTPS" : "HTTP"}</span>
|
<Button variant="outline" disabled>
|
||||||
</div>
|
{item.port}
|
||||||
|
</Button>
|
||||||
<div className="flex gap-2">
|
<Button variant="outline" disabled>
|
||||||
<AddDomain
|
{item.https ? "HTTPS" : "HTTP"}
|
||||||
applicationId={applicationId}
|
</Button>
|
||||||
domainId={item.domainId}
|
<div className="flex flex-row gap-1">
|
||||||
>
|
<AddDomain
|
||||||
<Button
|
applicationId={applicationId}
|
||||||
variant="ghost"
|
domainId={item.domainId}
|
||||||
size="icon"
|
>
|
||||||
className="group hover:bg-blue-500/10 "
|
<Button variant="ghost">
|
||||||
>
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
</Button>
|
||||||
</Button>
|
</AddDomain>
|
||||||
</AddDomain>
|
<DeleteDomain domainId={item.domainId} />
|
||||||
<DialogAction
|
|
||||||
title="Delete Domain"
|
|
||||||
description="Are you sure you want to delete this domain?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteDomain({
|
|
||||||
domainId: item.domainId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
toast.success("Domain deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting domain");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,17 +51,17 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error adding environment");
|
toast.error("Error to add environment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background px-6 pb-6">
|
<Form {...form}>
|
||||||
<Form {...form}>
|
<form
|
||||||
<form
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
className="flex w-full flex-col gap-5 "
|
||||||
className="flex w-full flex-col gap-4"
|
>
|
||||||
>
|
<Card className="bg-background p-6">
|
||||||
<Secrets
|
<Secrets
|
||||||
name="env"
|
name="env"
|
||||||
title="Environment Settings"
|
title="Environment Settings"
|
||||||
@@ -89,13 +89,15 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
placeholder="NPM_TOKEN=xyz"
|
placeholder="NPM_TOKEN=xyz"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row justify-end">
|
<CardContent>
|
||||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
<div className="flex flex-row justify-end">
|
||||||
Save
|
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||||
</Button>
|
Save
|
||||||
</div>
|
</Button>
|
||||||
</form>
|
</div>
|
||||||
</Form>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeployApplication = ({ applicationId }: Props) => {
|
||||||
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
|
{
|
||||||
|
applicationId,
|
||||||
|
},
|
||||||
|
{ enabled: !!applicationId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button isLoading={data?.applicationStatus === "running"}>
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will deploy the application
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await deploy({
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Application deployed succesfully");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to deploy Application");
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -137,7 +137,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the Bitbucket provider");
|
toast.error("Error to save the Bitbucket provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -245,12 +245,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
{repo.name}
|
||||||
<span>{repo.name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{repo.owner.username}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the Docker provider");
|
toast.error("Error to save the Docker provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the deployment");
|
toast.error("Error to save the deployment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the Git provider");
|
toast.error("Error to save the Git provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the github provider");
|
toast.error("Error to save the github provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -236,12 +236,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
{repo.name}
|
||||||
<span>{repo.name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{repo.owner.login}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the gitlab provider");
|
toast.error("Error to save the gitlab provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
{repositories?.map((repo) => {
|
{repositories?.map((repo) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -260,12 +260,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
{repo.name}
|
||||||
<span>{repo.name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{repo.owner.username}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
appName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResetApplication = ({ applicationId, appName }: Props) => {
|
||||||
|
const { refetch } = api.application.one.useQuery(
|
||||||
|
{
|
||||||
|
applicationId,
|
||||||
|
},
|
||||||
|
{ enabled: !!applicationId },
|
||||||
|
);
|
||||||
|
const { mutateAsync: reload, isLoading } =
|
||||||
|
api.application.reload.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="secondary" isLoading={isLoading}>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will reload the application
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
applicationId,
|
||||||
|
appName,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Service Reloaded");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to reload the service");
|
||||||
|
});
|
||||||
|
await refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
||||||
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
|
import { Terminal } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
import { RedbuildApplication } from "../rebuild-application";
|
||||||
|
import { StartApplication } from "../start-application";
|
||||||
|
import { StopApplication } from "../stop-application";
|
||||||
|
import { DeployApplication } from "./deploy-application";
|
||||||
|
import { ResetApplication } from "./reset-application";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||||
const router = useRouter();
|
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
@@ -23,18 +25,6 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
const { mutateAsync: update } = api.application.update.useMutation();
|
const { mutateAsync: update } = api.application.update.useMutation();
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
|
||||||
api.application.start.useMutation();
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
|
||||||
api.application.stop.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: deploy, isLoading: isDeploying } =
|
|
||||||
api.application.deploy.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
|
||||||
api.application.reload.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -43,127 +33,17 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DialogAction
|
<DeployApplication applicationId={applicationId} />
|
||||||
title="Deploy Application"
|
<ResetApplication
|
||||||
description="Are you sure you want to deploy this application?"
|
applicationId={applicationId}
|
||||||
type="default"
|
appName={data?.appName || ""}
|
||||||
onClick={async () => {
|
/>
|
||||||
await deploy({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application deployed successfully");
|
|
||||||
refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deploying application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Application"
|
|
||||||
description="Are you sure you want to reload this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
applicationId: applicationId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
|
||||||
Reload
|
|
||||||
<RefreshCcw className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Rebuild Application"
|
|
||||||
description="Are you sure you want to rebuild this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await redeploy({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application rebuilt successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error rebuilding application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Rebuild
|
|
||||||
<Hammer className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
|
|
||||||
|
<RedbuildApplication applicationId={applicationId} />
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<StartApplication applicationId={applicationId} />
|
||||||
title="Start Application"
|
|
||||||
description="Are you sure you want to start this application?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
|
||||||
Start
|
|
||||||
<CheckCircle2 className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<StopApplication applicationId={applicationId} />
|
||||||
title="Stop Application"
|
|
||||||
description="Are you sure you want to stop this application?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Application stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
|
||||||
Stop
|
|
||||||
<Ban className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -189,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating Auto Deploy");
|
toast.error("Error to update Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -16,7 +15,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
@@ -31,67 +29,28 @@ export const DockerLogs = dynamic(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const badgeStateColor = (state: string) => {
|
|
||||||
switch (state) {
|
|
||||||
case "running":
|
|
||||||
return "green";
|
|
||||||
case "exited":
|
|
||||||
case "shutdown":
|
|
||||||
return "red";
|
|
||||||
case "accepted":
|
|
||||||
case "created":
|
|
||||||
return "blue";
|
|
||||||
default:
|
|
||||||
return "default";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||||
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!appName,
|
||||||
|
},
|
||||||
|
);
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
|
||||||
|
|
||||||
const { data: services, isLoading: servicesLoading } =
|
|
||||||
api.docker.getServiceContainersByAppName.useQuery(
|
|
||||||
{
|
|
||||||
appName,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!appName && option === "swarm",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: containers, isLoading: containersLoading } =
|
|
||||||
api.docker.getContainersByAppNameMatch.useQuery(
|
|
||||||
{
|
|
||||||
appName,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!appName && option === "native",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (option === "native") {
|
if (data && data?.length > 0) {
|
||||||
if (containers && containers?.length > 0) {
|
setContainerId(data[0]?.containerId);
|
||||||
setContainerId(containers[0]?.containerId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (services && services?.length > 0) {
|
|
||||||
setContainerId(services[0]?.containerId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [option, services, containers]);
|
}, [data]);
|
||||||
|
|
||||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
|
||||||
const containersLenght =
|
|
||||||
option === "native" ? containers?.length : services?.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -103,21 +62,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<div className="flex flex-row justify-between items-center gap-2">
|
<Label>Select a container to view logs</Label>
|
||||||
<Label>Select a container to view logs</Label>
|
|
||||||
<div className="flex flex-row gap-2 items-center">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{option === "native" ? "Native" : "Swarm"}
|
|
||||||
</span>
|
|
||||||
<Switch
|
|
||||||
checked={option === "native"}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setOption(checked ? "native" : "swarm");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -131,45 +76,22 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{option === "native" ? (
|
{data?.map((container) => (
|
||||||
<div>
|
<SelectItem
|
||||||
{containers?.map((container) => (
|
key={container.containerId}
|
||||||
<SelectItem
|
value={container.containerId}
|
||||||
key={container.containerId}
|
>
|
||||||
value={container.containerId}
|
{container.name} ({container.containerId}) {container.state}
|
||||||
>
|
</SelectItem>
|
||||||
{container.name} ({container.containerId}){" "}
|
))}
|
||||||
<Badge variant={badgeStateColor(container.state)}>
|
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||||
{container.state}
|
|
||||||
</Badge>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{services?.map((container) => (
|
|
||||||
<SelectItem
|
|
||||||
key={container.containerId}
|
|
||||||
value={container.containerId}
|
|
||||||
>
|
|
||||||
{container.name} ({container.containerId}@{container.node}
|
|
||||||
)
|
|
||||||
<Badge variant={badgeStateColor(container.state)}>
|
|
||||||
{container.state}
|
|
||||||
</Badge>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
|
id="terminal"
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
runType={option}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ export const AddPreviewDomain = ({
|
|||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
error: domainId
|
||||||
|
? "Error to update the domain"
|
||||||
|
: "Error to create the domain",
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
@@ -263,21 +265,21 @@ export const AddPreviewDomain = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
<FormLabel>Certificate</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate provider" />
|
<SelectValue placeholder="Select a certificate" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Let's Encrypt
|
Letsencrypt (Default)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -18,28 +18,15 @@ import { ShowDeployment } from "../deployments/show-deployment";
|
|||||||
interface Props {
|
interface Props {
|
||||||
deployments: RouterOutputs["deployment"]["all"];
|
deployments: RouterOutputs["deployment"]["all"];
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
trigger?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPreviewBuilds = ({
|
export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
|
||||||
deployments,
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
serverId,
|
|
||||||
trigger,
|
|
||||||
}: Props) => {
|
|
||||||
const [activeLog, setActiveLog] = useState<
|
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
|
||||||
>(null);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{trigger ? (
|
<Button variant="outline">View Builds</Button>
|
||||||
trigger
|
|
||||||
) : (
|
|
||||||
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
|
||||||
View Builds
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -79,7 +66,7 @@ export const ShowPreviewBuilds = ({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveLog(deployment);
|
setActiveLog(deployment.logPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@@ -91,10 +78,9 @@ export const ShowPreviewBuilds = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
open={activeLog !== null}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog?.logPath || ""}
|
logPath={activeLog}
|
||||||
errorMessage={activeLog?.errorMessage || ""}
|
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -11,34 +8,30 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import { Pencil, RocketIcon } from "lucide-react";
|
||||||
ExternalLink,
|
import React, { useEffect, useState } from "react";
|
||||||
FileText,
|
|
||||||
GitPullRequest,
|
|
||||||
Layers,
|
|
||||||
PenSquare,
|
|
||||||
RocketIcon,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
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 { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { AddPreviewDomain } from "./add-preview-domain";
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
import { ShowPreviewBuilds } from "./show-preview-builds";
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { ShowPreviewSettings } from "./show-preview-settings";
|
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||||
|
import { ShowPreviewBuilds } from "./show-preview-builds";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||||
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
const { data } = api.application.one.useQuery({ applicationId });
|
const { data } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
api.previewDeployment.delete.useMutation();
|
api.previewDeployment.delete.useMutation();
|
||||||
|
|
||||||
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
||||||
api.previewDeployment.all.useQuery(
|
api.previewDeployment.all.useQuery(
|
||||||
{ applicationId },
|
{ applicationId },
|
||||||
@@ -46,19 +39,10 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// const [url, setUrl] = React.useState("");
|
||||||
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
// useEffect(() => {
|
||||||
deletePreviewDeployment({
|
// setUrl(document.location.origin);
|
||||||
previewDeploymentId: previewDeploymentId,
|
// }, []);
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetchPreviewDeployments();
|
|
||||||
toast.success("Preview deployment deleted");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -81,7 +65,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
each pull request you create.
|
each pull request you create.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{!previewDeployments?.length ? (
|
{data?.previewDeployments?.length === 0 ? (
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<RocketIcon className="size-8 text-muted-foreground" />
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
@@ -90,131 +74,120 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{previewDeployments?.map((deployment) => {
|
{previewDeployments?.map((previewDeployment) => {
|
||||||
const deploymentUrl = `${deployment.domain?.https ? "https" : "http"}://${deployment.domain?.host}${deployment.domain?.path || "/"}`;
|
const { deployments, domain } = previewDeployment;
|
||||||
const status = deployment.previewStatus;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={deployment.previewDeploymentId}
|
key={previewDeployment?.previewDeploymentId}
|
||||||
className="group relative overflow-hidden border rounded-lg transition-colors"
|
className="flex flex-col justify-between rounded-lg border p-4 gap-2"
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex justify-between gap-2 max-sm:flex-wrap">
|
||||||
className={`absolute left-0 top-0 w-1 h-full ${
|
<div className="flex flex-col gap-2">
|
||||||
status === "done"
|
{deployments?.length === 0 ? (
|
||||||
? "bg-green-500"
|
|
||||||
: status === "running"
|
|
||||||
? "bg-yellow-500"
|
|
||||||
: "bg-red-500"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex items-start justify-between mb-3">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" />
|
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-sm">
|
<span className="text-sm text-muted-foreground">
|
||||||
{deployment.pullRequestTitle}
|
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>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground mt-1">
|
)}
|
||||||
{deployment.branch}
|
|
||||||
|
{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>
|
||||||
|
<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>
|
||||||
<Badge variant="outline" className="gap-2">
|
|
||||||
<StatusTooltip
|
|
||||||
status={deployment.previewStatus}
|
|
||||||
className="size-2"
|
|
||||||
/>
|
|
||||||
<DateTooltip date={deployment.createdAt} />
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pl-8 space-y-3">
|
<div className="flex flex-col sm:items-end gap-2 max-sm:w-full">
|
||||||
<div className="relative flex-grow">
|
{previewDeployment?.createdAt && (
|
||||||
<Input
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
value={deploymentUrl}
|
<DateTooltip
|
||||||
readOnly
|
date={previewDeployment?.createdAt}
|
||||||
className="pr-8 text-sm text-blue-500 hover:text-blue-600 cursor-pointer"
|
/>
|
||||||
onClick={() =>
|
</div>
|
||||||
window.open(deploymentUrl, "_blank")
|
)}
|
||||||
}
|
<ShowPreviewBuilds
|
||||||
/>
|
deployments={previewDeployment?.deployments || []}
|
||||||
<ExternalLink className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" />
|
serverId={data?.serverId || ""}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-2 opacity-80 group-hover:opacity-100 transition-opacity">
|
<ShowModalLogs
|
||||||
<Button
|
appName={previewDeployment.appName}
|
||||||
variant="outline"
|
serverId={data?.serverId || ""}
|
||||||
size="sm"
|
>
|
||||||
className="gap-2"
|
<Button variant="outline">View Logs</Button>
|
||||||
onClick={() =>
|
</ShowModalLogs>
|
||||||
window.open(deployment.pullRequestURL, "_blank")
|
|
||||||
}
|
<DialogAction
|
||||||
>
|
title="Delete Preview"
|
||||||
<GithubIcon className="size-4" />
|
description="Are you sure you want to delete this preview?"
|
||||||
Pull Request
|
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>
|
</Button>
|
||||||
<ShowModalLogs
|
</DialogAction>
|
||||||
appName={deployment.appName}
|
|
||||||
serverId={data?.serverId || ""}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<FileText className="size-4" />
|
|
||||||
Logs
|
|
||||||
</Button>
|
|
||||||
</ShowModalLogs>
|
|
||||||
|
|
||||||
<ShowPreviewBuilds
|
|
||||||
deployments={deployment.deployments || []}
|
|
||||||
serverId={data?.serverId || ""}
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<Layers className="size-4" />
|
|
||||||
Builds
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AddPreviewDomain
|
|
||||||
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
|
||||||
domainId={deployment.domain?.domainId}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<PenSquare className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</AddPreviewDomain>
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Preview"
|
|
||||||
description="Are you sure you want to delete this preview?"
|
|
||||||
onClick={() =>
|
|
||||||
handleDeletePreviewDeployment(
|
|
||||||
deployment.previewDeploymentId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
isLoading={isLoading}
|
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -18,7 +20,12 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input, NumberInput } from "@/components/ui/input";
|
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 { Secrets } from "@/components/ui/secrets";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -26,14 +33,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Settings2 } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
@@ -117,10 +116,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">
|
<Button variant="outline">View Settings</Button>
|
||||||
<Settings2 className="size-4" />
|
|
||||||
Configure
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -222,21 +218,21 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
name="previewCertificateType"
|
name="previewCertificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
<FormLabel>Certificate</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate provider" />
|
<SelectValue placeholder="Select a certificate" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Let's Encrypt
|
Letsencrypt (Default)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -291,6 +287,16 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
"PORT=3000",
|
"PORT=3000",
|
||||||
].join("\n")}
|
].join("\n")}
|
||||||
/>
|
/>
|
||||||
|
{/* <CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
language="properties"
|
||||||
|
wrapperClassName="h-[25rem] font-mono"
|
||||||
|
placeholder={`NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/> */}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Hammer } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RedbuildApplication = ({ applicationId }: Props) => {
|
||||||
|
const { data } = api.application.one.useQuery(
|
||||||
|
{
|
||||||
|
applicationId,
|
||||||
|
},
|
||||||
|
{ enabled: !!applicationId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync } = api.application.redeploy.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure to rebuild the application?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Is required to deploy at least 1 time in order to reuse the same
|
||||||
|
code
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
toast.success("Redeploying Application....");
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.application.one.invalidate({
|
||||||
|
applicationId,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to rebuild the application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { CheckCircle2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StartApplication = ({ applicationId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.application.start.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="secondary" isLoading={isLoading}>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure to start the application?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will start the application
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.application.one.invalidate({
|
||||||
|
applicationId,
|
||||||
|
});
|
||||||
|
toast.success("Application started succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to start the Application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Ban } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StopApplication = ({ applicationId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.application.stop.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive" isLoading={isLoading}>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you absolutely sure to stop the application?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will stop the application
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
applicationId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.application.one.invalidate({
|
||||||
|
applicationId,
|
||||||
|
});
|
||||||
|
toast.success("Application stopped succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to stop the Application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -76,14 +76,14 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
description: formData.description || "",
|
description: formData.description || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application updated successfully");
|
toast.success("Application updated succesfully");
|
||||||
utils.application.one.invalidate({
|
utils.application.one.invalidate({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the Application");
|
toast.error("Error to update the application");
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
@@ -91,12 +91,8 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button variant="ghost">
|
||||||
variant="ghost"
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10 "
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -82,7 +81,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the command");
|
toast.error("Error to update the command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Run Command</CardTitle>
|
<CardTitle className="text-xl">Run Command</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Override a custom command to the compose file
|
Append a custom command to the compose file
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -102,12 +101,6 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4"
|
className="grid w-full gap-4"
|
||||||
>
|
>
|
||||||
<AlertBlock type="warning">
|
|
||||||
Modifying the default command may affect deployment stability,
|
|
||||||
impacting logs and monitoring. Proceed carefully and test
|
|
||||||
thoroughly. By default, the command starts with{" "}
|
|
||||||
<strong>docker</strong>.
|
|
||||||
</AlertBlock>
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Package } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { AddVolumes } from "../../application/advanced/volumes/add-volumes";
|
||||||
|
import { DeleteVolume } from "../../application/advanced/volumes/delete-volume";
|
||||||
|
import { UpdateVolume } from "../../application/advanced/volumes/update-volume";
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowVolumesCompose = ({ composeId }: Props) => {
|
||||||
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
If you want to persist data in this compose use the following config
|
||||||
|
to setup the volumes
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data && data?.mounts.length > 0 && (
|
||||||
|
<AddVolumes
|
||||||
|
serviceId={composeId}
|
||||||
|
refetch={refetch}
|
||||||
|
serviceType="compose"
|
||||||
|
>
|
||||||
|
Add Volume
|
||||||
|
</AddVolumes>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{data?.mounts.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<Package className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
No volumes/mounts configured
|
||||||
|
</span>
|
||||||
|
<AddVolumes
|
||||||
|
serviceId={composeId}
|
||||||
|
refetch={refetch}
|
||||||
|
serviceType="compose"
|
||||||
|
>
|
||||||
|
Add Volume
|
||||||
|
</AddVolumes>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
|
<AlertBlock type="info">
|
||||||
|
Please remember to click Redeploy after adding, editing, or
|
||||||
|
deleting a mount to apply the changes.
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{data?.mounts.map((mount) => (
|
||||||
|
<div key={mount.mountId}>
|
||||||
|
<div
|
||||||
|
key={mount.mountId}
|
||||||
|
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Mount Type</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.type.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{mount.type === "volume" && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Volume Name</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.volumeName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mount.type === "file" && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Content</span>
|
||||||
|
<span className="text-sm text-muted-foreground w-40 truncate">
|
||||||
|
{mount.content}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">File Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.filePath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{mount.type === "bind" && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Host Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.hostPath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Mount Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.mountPath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-1">
|
||||||
|
<UpdateVolume
|
||||||
|
mountId={mount.mountId}
|
||||||
|
type={mount.type}
|
||||||
|
refetch={refetch}
|
||||||
|
serviceType="compose"
|
||||||
|
/>
|
||||||
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
141
apps/dokploy/components/dashboard/compose/delete-compose.tsx
Normal file
141
apps/dokploy/components/dashboard/compose/delete-compose.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
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,226 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import type { ServiceType } from "@dokploy/server/db/schema";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { Copy, Trash2 } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const deleteComposeSchema = z.object({
|
|
||||||
projectName: z.string().min(1, {
|
|
||||||
message: "Compose name is required",
|
|
||||||
}),
|
|
||||||
deleteVolumes: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id: string;
|
|
||||||
type: ServiceType | "application";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteService = ({ id, type }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const queryMap = {
|
|
||||||
postgres: () =>
|
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
|
||||||
mariadb: () =>
|
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
|
||||||
application: () =>
|
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
|
||||||
compose: () =>
|
|
||||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
|
||||||
};
|
|
||||||
const { data, refetch } = queryMap[type]
|
|
||||||
? queryMap[type]()
|
|
||||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
|
||||||
|
|
||||||
const mutationMap = {
|
|
||||||
postgres: () => api.postgres.remove.useMutation(),
|
|
||||||
redis: () => api.redis.remove.useMutation(),
|
|
||||||
mysql: () => api.mysql.remove.useMutation(),
|
|
||||||
mariadb: () => api.mariadb.remove.useMutation(),
|
|
||||||
application: () => api.application.delete.useMutation(),
|
|
||||||
mongo: () => api.mongo.remove.useMutation(),
|
|
||||||
compose: () => api.compose.delete.useMutation(),
|
|
||||||
};
|
|
||||||
const { mutateAsync, isLoading } = mutationMap[type]
|
|
||||||
? mutationMap[type]()
|
|
||||||
: api.mongo.remove.useMutation();
|
|
||||||
const { push } = useRouter();
|
|
||||||
const form = useForm<DeleteCompose>({
|
|
||||||
defaultValues: {
|
|
||||||
projectName: "",
|
|
||||||
deleteVolumes: false,
|
|
||||||
},
|
|
||||||
resolver: zodResolver(deleteComposeSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (formData: DeleteCompose) => {
|
|
||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
|
||||||
if (formData.projectName === expectedName) {
|
|
||||||
const { deleteVolumes } = formData;
|
|
||||||
await mutateAsync({
|
|
||||||
mongoId: id || "",
|
|
||||||
postgresId: id || "",
|
|
||||||
redisId: id || "",
|
|
||||||
mysqlId: id || "",
|
|
||||||
mariadbId: id || "",
|
|
||||||
applicationId: id || "",
|
|
||||||
composeId: id || "",
|
|
||||||
deleteVolumes,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
push(`/dashboard/project/${result?.projectId}`);
|
|
||||||
toast.success("deleted successfully");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the service");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.setError("projectName", {
|
|
||||||
message: `Project name must match "${expectedName}"`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10 "
|
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</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
|
|
||||||
service. If you are sure please enter the service name to delete
|
|
||||||
this service.
|
|
||||||
</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 className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
To confirm, type{" "}
|
|
||||||
<Badge
|
|
||||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (data?.name && data?.appName) {
|
|
||||||
copy(`${data.name}/${data.appName}`);
|
|
||||||
toast.success("Copied to clipboard. Be careful!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data?.name}/{data?.appName}
|
|
||||||
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
|
||||||
</Badge>{" "}
|
|
||||||
in the box below:
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter compose name to confirm"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{type === "compose" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="deleteVolumes"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormLabel className="ml-2">
|
|
||||||
Delete volumes associated with this compose
|
|
||||||
</FormLabel>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -20,11 +20,6 @@ interface Props {
|
|||||||
|
|
||||||
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
|
|
||||||
if (isCloud) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const RefreshTokenCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Refresh Token updated");
|
toast.success("Refresh Token updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the refresh token");
|
toast.error("Error to update the refresh token");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -7,45 +5,23 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
errorMessage?: string;
|
|
||||||
}
|
}
|
||||||
export const ShowDeploymentCompose = ({
|
export const ShowDeploymentCompose = ({
|
||||||
logPath,
|
logPath,
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
serverId,
|
serverId,
|
||||||
errorMessage,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
if (autoScroll && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!scrollRef.current) return;
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
||||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
|
||||||
setAutoScroll(isAtBottom);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
@@ -78,36 +54,13 @@ export const ShowDeploymentCompose = ({
|
|||||||
};
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
useEffect(() => {
|
const scrollToBottom = () => {
|
||||||
const logs = parseLogs(data);
|
endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
let filteredLogsResult = logs;
|
};
|
||||||
if (serverId) {
|
|
||||||
let hideSubsequentLogs = false;
|
|
||||||
filteredLogsResult = logs.filter((log) => {
|
|
||||||
if (
|
|
||||||
log.message.includes(
|
|
||||||
"===================================EXTRA LOGS============================================",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
hideSubsequentLogs = true;
|
|
||||||
return showExtraLogs;
|
|
||||||
}
|
|
||||||
return showExtraLogs ? true : !hideSubsequentLogs;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilteredLogs(filteredLogsResult);
|
|
||||||
}, [data, showExtraLogs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
}, [data]);
|
||||||
if (autoScroll && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [filteredLogs, autoScroll]);
|
|
||||||
|
|
||||||
const optionalErrors = parseLogs(errorMessage || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -125,58 +78,21 @@ export const ShowDeploymentCompose = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className={"sm:max-w-5xl max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription className="flex items-center gap-2">
|
<DialogDescription>
|
||||||
<span>
|
See all the details of this deployment
|
||||||
See all the details of this deployment |{" "}
|
|
||||||
<Badge variant="blank" className="text-xs">
|
|
||||||
{filteredLogs.length} lines
|
|
||||||
</Badge>
|
|
||||||
</span>
|
|
||||||
{serverId && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="show-extra-logs"
|
|
||||||
checked={showExtraLogs}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setShowExtraLogs(checked as boolean)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="show-extra-logs"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
Show Extra Logs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div
|
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem]">
|
||||||
ref={scrollRef}
|
<code>
|
||||||
onScroll={handleScroll}
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
{data || "Loading..."}
|
||||||
>
|
</pre>
|
||||||
{filteredLogs.length > 0 ? (
|
<div ref={endOfLogsRef} />
|
||||||
filteredLogs.map((log: LogLine, index: number) => (
|
</code>
|
||||||
<TerminalLine key={index} log={log} noTimestamp />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{optionalErrors.length > 0 ? (
|
|
||||||
optionalErrors.map((log: LogLine, index: number) => (
|
|
||||||
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RocketIcon } from "lucide-react";
|
import { RocketIcon } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
||||||
@@ -19,9 +19,7 @@ interface Props {
|
|||||||
composeId: string;
|
composeId: string;
|
||||||
}
|
}
|
||||||
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
|
||||||
>(null);
|
|
||||||
const { data } = api.compose.one.useQuery({ composeId });
|
const { data } = api.compose.one.useQuery({ composeId });
|
||||||
const { data: deployments } = api.deployment.allByCompose.useQuery(
|
const { data: deployments } = api.deployment.allByCompose.useQuery(
|
||||||
{ composeId },
|
{ composeId },
|
||||||
@@ -102,7 +100,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveLog(deployment);
|
setActiveLog(deployment.logPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@@ -114,10 +112,9 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
<ShowDeploymentCompose
|
<ShowDeploymentCompose
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
open={activeLog !== null}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog?.logPath || ""}
|
logPath={activeLog}
|
||||||
errorMessage={activeLog?.errorMessage || ""}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -126,7 +126,9 @@ export const AddDomainCompose = ({
|
|||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
error: domainId
|
||||||
|
? "Error to update the domain"
|
||||||
|
: "Error to create the domain",
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
@@ -398,21 +400,21 @@ export const AddDomainCompose = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
<FormLabel>Certificate</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate provider" />
|
<SelectValue placeholder="Select a certificate" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Let's Encrypt
|
Letsencrypt (Default)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -7,10 +6,11 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
|
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { DeleteDomain } from "../../application/domains/delete-domain";
|
||||||
import { AddDomainCompose } from "./add-domain";
|
import { AddDomainCompose } from "./add-domain";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -18,7 +18,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||||
const { data, refetch } = api.domain.byComposeId.useQuery(
|
const { data } = api.domain.byComposeId.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
},
|
},
|
||||||
@@ -27,9 +27,6 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
|
||||||
api.domain.delete.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -73,70 +70,34 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.domainId}
|
key={item.domainId}
|
||||||
className="flex w-full items-center justify-between gap-4 border p-4 md:px-6 rounded-lg flex-wrap"
|
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="md:basis-1/2 flex gap-6 w-full items-center">
|
<Link target="_blank" href={`http://${item.host}`}>
|
||||||
<span className="opacity-50 text-center font-medium text-sm whitespace-nowrap">
|
<ExternalLink className="size-5" />
|
||||||
{item.serviceName}
|
</Link>
|
||||||
</span>
|
<Button variant="outline" disabled>
|
||||||
|
{item.serviceName}
|
||||||
<Link
|
</Button>
|
||||||
className="flex gap-2 items-center hover:underline transition-all w-full max-w-[calc(100%-4rem)]"
|
<Input disabled value={item.host} />
|
||||||
target="_blank"
|
<Button variant="outline" disabled>
|
||||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
{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}
|
||||||
>
|
>
|
||||||
<span className="truncate text-sm">{item.host}</span>
|
<Button variant="ghost">
|
||||||
<ExternalLink className="size-4 min-w-4" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</AddDomainCompose>
|
||||||
|
<DeleteDomain domainId={item.domainId} />
|
||||||
<div className="flex gap-8">
|
|
||||||
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
|
|
||||||
<span>{item.path}</span>
|
|
||||||
<span>{item.port}</span>
|
|
||||||
<span>{item.https ? "HTTPS" : "HTTP"}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<AddDomainCompose
|
|
||||||
composeId={composeId}
|
|
||||||
domainId={item.domainId}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10 "
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
|
||||||
</AddDomainCompose>
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Domain"
|
|
||||||
description="Are you sure you want to delete this domain?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteDomain({
|
|
||||||
domainId: item.domainId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
refetch();
|
|
||||||
toast.success("Domain deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting domain");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
167
apps/dokploy/components/dashboard/compose/enviroment/show.tsx
Normal file
167
apps/dokploy/components/dashboard/compose/enviroment/show.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const addEnvironmentSchema = z.object({
|
||||||
|
environment: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
|
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||||
|
|
||||||
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!composeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const form = useForm<EnvironmentSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
environment: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(addEnvironmentSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
environment: data.env || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: EnvironmentSchema) => {
|
||||||
|
mutateAsync({
|
||||||
|
env: data.environment,
|
||||||
|
composeId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Environments Added");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to add environment");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEnvVisible) {
|
||||||
|
if (data?.env) {
|
||||||
|
const maskedLines = data.env
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => "*".repeat(line.length))
|
||||||
|
.join("\n");
|
||||||
|
form.reset({
|
||||||
|
environment: maskedLines,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.reset({
|
||||||
|
environment: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form.reset({
|
||||||
|
environment: data?.env || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form.reset, data, form, isEnvVisible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
You can add environment variables to your resource.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle bold"
|
||||||
|
pressed={isEnvVisible}
|
||||||
|
onPressedChange={setIsEnvVisible}
|
||||||
|
>
|
||||||
|
{isEnvVisible ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Toggle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="w-full space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="environment"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
|
placeholder={`NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
`}
|
||||||
|
className="h-96 font-mono"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-end">
|
||||||
|
<Button
|
||||||
|
disabled={isEnvVisible}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,17 +1,28 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, Hammer, Terminal } from "lucide-react";
|
import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
import { StartCompose } from "../start-compose";
|
||||||
|
import { DeployCompose } from "./deploy-compose";
|
||||||
|
import { RedbuildCompose } from "./rebuild-compose";
|
||||||
|
import { StopCompose } from "./stop-compose";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
}
|
}
|
||||||
export const ComposeActions = ({ composeId }: Props) => {
|
export const ComposeActions = ({ composeId }: Props) => {
|
||||||
const router = useRouter();
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
@@ -19,109 +30,33 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
{ enabled: !!composeId },
|
{ enabled: !!composeId },
|
||||||
);
|
);
|
||||||
const { mutateAsync: update } = api.compose.update.useMutation();
|
const { mutateAsync: update } = api.compose.update.useMutation();
|
||||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
|
||||||
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
|
const extractDomains = (env: string) => {
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const lines = env.split("\n");
|
||||||
api.compose.start.useMutation();
|
const hostLines = lines.filter((line) => {
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const [key, value] = line.split("=");
|
||||||
api.compose.stop.useMutation();
|
return key?.trim().endsWith("_HOST");
|
||||||
|
});
|
||||||
|
|
||||||
|
const hosts = hostLines.map((line) => {
|
||||||
|
const [key, value] = line.split("=");
|
||||||
|
return value ? value.trim() : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const domains = extractDomains(data?.env || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||||
<DialogAction
|
<DeployCompose composeId={composeId} />
|
||||||
title="Deploy Compose"
|
<RedbuildCompose composeId={composeId} />
|
||||||
description="Are you sure you want to deploy this compose?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await deploy({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose deployed successfully");
|
|
||||||
refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deploying compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="default" isLoading={data?.composeStatus === "running"}>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Rebuild Compose"
|
|
||||||
description="Are you sure you want to rebuild this compose?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await redeploy({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose rebuilt successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error rebuilding compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={data?.composeStatus === "running"}
|
|
||||||
>
|
|
||||||
Rebuild
|
|
||||||
<Hammer className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.composeType === "docker-compose" &&
|
{data?.composeType === "docker-compose" &&
|
||||||
data?.composeStatus === "idle" ? (
|
data?.composeStatus === "idle" ? (
|
||||||
<DialogAction
|
<StartCompose composeId={composeId} />
|
||||||
title="Start Compose"
|
|
||||||
description="Are you sure you want to start this compose?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" isLoading={isStarting}>
|
|
||||||
Start
|
|
||||||
<CheckCircle2 className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<StopCompose composeId={composeId} />
|
||||||
title="Stop Compose"
|
|
||||||
description="Are you sure you want to stop this compose?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="destructive" isLoading={isStopping}>
|
|
||||||
Stop
|
|
||||||
<Ban className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
@@ -148,12 +83,47 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating Auto Deploy");
|
toast.error("Error to update Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{domains.length > 0 && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
Domains
|
||||||
|
<Globe className="text-xs size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-56">
|
||||||
|
<DropdownMenuLabel>Domains detected</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
{domains.map((host, index) => {
|
||||||
|
const url =
|
||||||
|
host.startsWith("http://") || host.startsWith("https://")
|
||||||
|
? host
|
||||||
|
: `http://${host}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={`domain-${index}`}
|
||||||
|
className="cursor-pointer"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href={url} target="_blank">
|
||||||
|
{host}
|
||||||
|
<ExternalLink className="ml-2 text-xs text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
|
||||||
import { ShowUtilities } from "./show-utilities";
|
import { RandomizeCompose } from "./randomize-compose";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
@@ -77,7 +77,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast.error("Error updating the Compose config");
|
toast.error("Error to update the compose config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -125,7 +125,7 @@ services:
|
|||||||
</Form>
|
</Form>
|
||||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
|
||||||
<ShowUtilities composeId={composeId} />
|
<RandomizeCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeployCompose = ({ composeId }: Props) => {
|
||||||
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button isLoading={data?.composeStatus === "running"}>Deploy</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will deploy the compose
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
toast.success("Deploying Compose....");
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
await deploy({
|
||||||
|
composeId,
|
||||||
|
}).catch(() => {
|
||||||
|
toast.error("Error to deploy Compose");
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -139,7 +139,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the Bitbucket provider");
|
toast.error("Error to save the Bitbucket provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -247,12 +247,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
{repo.name}
|
||||||
<span>{repo.name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{repo.owner.username}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the Git provider");
|
toast.error("Error to save the Git provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the Github provider");
|
toast.error("Error to save the github provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -238,12 +238,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
{repo.name}
|
||||||
<span>{repo.name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{repo.owner.login}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the Gitlab provider");
|
toast.error("Error to save the gitlab provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
{repositories?.map((repo) => {
|
{repositories?.map((repo) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={repo.url}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -262,12 +262,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
{repo.name}
|
||||||
<span>{repo.name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{repo.owner.username}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
isolatedDeployment: z.boolean().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Schema = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
export const IsolatedDeployment = ({ composeId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [compose, setCompose] = useState<string>("");
|
|
||||||
const { mutateAsync, error, isError } =
|
|
||||||
api.compose.isolatedDeployment.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
|
||||||
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
|
||||||
{ composeId },
|
|
||||||
{ enabled: !!composeId },
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
|
||||||
defaultValues: {
|
|
||||||
isolatedDeployment: false,
|
|
||||||
},
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
randomizeCompose();
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
isolatedDeployment: data?.isolatedDeployment || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: Schema) => {
|
|
||||||
await updateCompose({
|
|
||||||
composeId,
|
|
||||||
isolatedDeployment: formData?.isolatedDeployment || false,
|
|
||||||
})
|
|
||||||
.then(async (data) => {
|
|
||||||
randomizeCompose();
|
|
||||||
refetch();
|
|
||||||
toast.success("Compose updated");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the compose");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const randomizeCompose = async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
suffix: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(async (data) => {
|
|
||||||
await utils.project.all.invalidate();
|
|
||||||
setCompose(data);
|
|
||||||
toast.success("Compose Isolated");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error isolating the compose");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Isolate Deployment</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Use this option to isolate the deployment of this compose file.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
|
||||||
<span>
|
|
||||||
This feature creates an isolated environment for your deployment by
|
|
||||||
adding unique prefixes to all resources. It establishes a dedicated
|
|
||||||
network based on your compose file's name, ensuring your services run
|
|
||||||
in isolation. This prevents conflicts when running multiple instances
|
|
||||||
of the same template or services with identical names.
|
|
||||||
</span>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">
|
|
||||||
Resources that will be isolated:
|
|
||||||
</h4>
|
|
||||||
<ul className="list-disc list-inside">
|
|
||||||
<li>Docker volumes</li>
|
|
||||||
<li>Docker networks</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-add-project"
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
|
||||||
<div>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="isolatedDeployment"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Enable Randomize ({data?.appName})</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Enable randomize to the compose file.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
|
||||||
<Button
|
|
||||||
form="hook-form-add-project"
|
|
||||||
type="submit"
|
|
||||||
className="lg:w-fit"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Label>Preview</Label>
|
|
||||||
<pre>
|
|
||||||
<CodeEditor
|
|
||||||
value={compose || ""}
|
|
||||||
language="yaml"
|
|
||||||
readOnly
|
|
||||||
height="50rem"
|
|
||||||
/>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -16,6 +20,11 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
InputOTP,
|
||||||
|
InputOTPGroup,
|
||||||
|
InputOTPSlot,
|
||||||
|
} from "@/components/ui/input-otp";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -61,7 +70,6 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
const suffix = form.watch("suffix");
|
const suffix = form.watch("suffix");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
randomizeCompose();
|
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
suffix: data?.suffix || "",
|
suffix: data?.suffix || "",
|
||||||
@@ -82,7 +90,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Compose updated");
|
toast.success("Compose updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error randomizing the compose");
|
toast.error("Error to randomize the compose");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,122 +105,131 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Compose randomized");
|
toast.success("Compose randomized");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error randomizing the compose");
|
toast.error("Error to randomize the compose");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogHeader>
|
<DialogTrigger asChild onClick={() => randomizeCompose()}>
|
||||||
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
<Button className="max-lg:w-full" variant="outline">
|
||||||
<DialogDescription>
|
<Dices className="h-4 w-4" />
|
||||||
Use this in case you want to deploy the same compose file and you have
|
Randomize Compose
|
||||||
conflicts with some property like volumes, networks, etc.
|
</Button>
|
||||||
</DialogDescription>
|
</DialogTrigger>
|
||||||
</DialogHeader>
|
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
||||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
<DialogHeader>
|
||||||
<span>
|
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
|
||||||
This will randomize the compose file and will add a suffix to the
|
<DialogDescription>
|
||||||
property to avoid conflicts
|
Use this in case you want to deploy the same compose file and you
|
||||||
</span>
|
have conflicts with some property like volumes, networks, etc.
|
||||||
<ul className="list-disc list-inside">
|
</DialogDescription>
|
||||||
<li>volumes</li>
|
</DialogHeader>
|
||||||
<li>networks</li>
|
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||||
<li>services</li>
|
<span>
|
||||||
<li>configs</li>
|
This will randomize the compose file and will add a suffix to the
|
||||||
<li>secrets</li>
|
property to avoid conflicts
|
||||||
</ul>
|
</span>
|
||||||
<AlertBlock type="info">
|
<ul className="list-disc list-inside">
|
||||||
When you activate this option, we will include a env `COMPOSE_PREFIX`
|
<li>volumes</li>
|
||||||
variable to the compose file so you can use it in your compose file.
|
<li>networks</li>
|
||||||
</AlertBlock>
|
<li>services</li>
|
||||||
</div>
|
<li>configs</li>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
<li>secrets</li>
|
||||||
<Form {...form}>
|
</ul>
|
||||||
<form
|
<AlertBlock type="info">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
When you activate this option, we will include a env
|
||||||
id="hook-form-add-project"
|
`COMPOSE_PREFIX` variable to the compose file so you can use it in
|
||||||
className="grid w-full gap-4"
|
your compose file.
|
||||||
>
|
</AlertBlock>
|
||||||
{isError && (
|
</div>
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
<Form {...form}>
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
<form
|
||||||
{error?.message}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
</span>
|
id="hook-form-add-project"
|
||||||
</div>
|
className="grid w-full gap-4"
|
||||||
)}
|
>
|
||||||
|
{isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
||||||
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{error?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
<div className="flex flex-col lg:flex-col gap-4 w-full ">
|
||||||
<div>
|
<div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="suffix"
|
name="suffix"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col justify-center max-sm:items-center w-full mt-4">
|
<FormItem className="flex flex-col justify-center max-sm:items-center w-full">
|
||||||
<FormLabel>Suffix</FormLabel>
|
<FormLabel>Suffix</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a suffix (Optional, example: prod)"
|
placeholder="Enter a suffix (Optional, example: prod)"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="randomize"
|
name="randomize"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Apply Randomize</FormLabel>
|
<FormLabel>Apply Randomize</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Apply randomize to the compose file.
|
Apply randomize to the compose file.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
|
||||||
<Button
|
<Button
|
||||||
form="hook-form-add-project"
|
form="hook-form-add-project"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="lg:w-fit"
|
className="lg:w-fit"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await randomizeCompose();
|
await randomizeCompose();
|
||||||
}}
|
}}
|
||||||
className="lg:w-fit"
|
className="lg:w-fit"
|
||||||
>
|
>
|
||||||
Random
|
Random
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<pre>
|
||||||
<pre>
|
<CodeEditor
|
||||||
<CodeEditor
|
value={compose || ""}
|
||||||
value={compose || ""}
|
language="yaml"
|
||||||
language="yaml"
|
readOnly
|
||||||
readOnly
|
height="50rem"
|
||||||
height="50rem"
|
/>
|
||||||
/>
|
</pre>
|
||||||
</pre>
|
</form>
|
||||||
</form>
|
</Form>
|
||||||
</Form>
|
</DialogContent>
|
||||||
</div>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Hammer } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RedbuildCompose = ({ composeId }: Props) => {
|
||||||
|
const { data } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
|
const { mutateAsync } = api.compose.redeploy.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={data?.composeStatus === "running"}
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure to rebuild the compose?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Is required to deploy at least 1 time in order to reuse the same
|
||||||
|
code
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
toast.success("Redeploying Compose....");
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.compose.one.invalidate({
|
||||||
|
composeId,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to rebuild the compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -73,7 +73,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Fetched source type");
|
toast.success("Fetched source type");
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error("Error fetching source type", {
|
toast.error("Error to fetch source type", {
|
||||||
description: err.message,
|
description: err.message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { IsolatedDeployment } from "./isolated-deployment";
|
|
||||||
import { RandomizeCompose } from "./randomize-compose";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowUtilities = ({ composeId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost">Show Utilities</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Utilities </DialogTitle>
|
|
||||||
<DialogDescription>Modify the application data</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Tabs defaultValue="isolated">
|
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
|
||||||
<TabsTrigger value="isolated">Isolated Deployment</TabsTrigger>
|
|
||||||
<TabsTrigger value="randomize">Randomize Compose</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="randomize" className="pt-5">
|
|
||||||
<RandomizeCompose composeId={composeId} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="isolated" className="pt-5">
|
|
||||||
<IsolatedDeployment composeId={composeId} />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Ban } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StopCompose = ({ composeId }: Props) => {
|
||||||
|
const { data } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{ enabled: !!composeId },
|
||||||
|
);
|
||||||
|
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive" isLoading={isLoading}>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure to stop the compose?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will stop the compose services
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.compose.one.invalidate({
|
||||||
|
composeId,
|
||||||
|
});
|
||||||
|
toast.success("Compose stopped succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to stop the compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
161
apps/dokploy/components/dashboard/compose/import-template.tsx
Normal file
161
apps/dokploy/components/dashboard/compose/import-template.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ImportIcon, SquarePen } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const updateComposeSchema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
|
description: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type UpdateCompose = z.infer<typeof updateComposeSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportTemplate = ({ composeId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
|
api.compose.update.useMutation();
|
||||||
|
const { data } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!composeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const form = useForm<UpdateCompose>({
|
||||||
|
defaultValues: {
|
||||||
|
description: data?.description ?? "",
|
||||||
|
name: data?.name ?? "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(updateComposeSchema),
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
description: data.description ?? "",
|
||||||
|
name: data.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: UpdateCompose) => {
|
||||||
|
await mutateAsync({
|
||||||
|
name: formData.name,
|
||||||
|
composeId: composeId,
|
||||||
|
description: formData.description || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose updated succesfully");
|
||||||
|
utils.compose.one.invalidate({
|
||||||
|
composeId: composeId,
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the Compose");
|
||||||
|
})
|
||||||
|
.finally(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<ImportIcon className="size-5 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Import Template</DialogTitle>
|
||||||
|
<DialogDescription>Import external template</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid items-center gap-4">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id="hook-form-update-compose"
|
||||||
|
className="grid w-full gap-4 "
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Tesla" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Description about your project..."
|
||||||
|
className="resize-none"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-update-compose"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user