mirror of
https://github.com/LukeHagar/dokploy.git
synced 2025-12-06 04:19:37 +00:00
Revert "Merge branch 'canary' into kucherenko/canary"
This reverts commit819822f30b, reversing changes made tobda9b05134.
This commit is contained in:
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:20.9.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
|
||||||
|
- feat/add-sidebar
|
||||||
|
- build-arm64:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- canary
|
||||||
|
- feat/add-sidebar
|
||||||
|
- combine-manifests:
|
||||||
|
requires:
|
||||||
|
- build-amd64
|
||||||
|
- build-arm64
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- canary
|
||||||
|
- feat/add-sidebar
|
||||||
BIN
.github/sponsors/openalternative.png
vendored
BIN
.github/sponsors/openalternative.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 KiB |
BIN
.github/sponsors/synexa.png
vendored
BIN
.github/sponsors/synexa.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 41 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/better-auth-2"]
|
|
||||||
|
|
||||||
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
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,6 +39,3 @@ yarn-error.log*
|
|||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
|
||||||
.db
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.9-slim AS base
|
FROM node:20-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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.9-slim AS base
|
FROM node:20-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
|
||||||
|
|||||||
@@ -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:20-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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20.9-slim AS base
|
FROM node:20-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
|
||||||
|
|||||||
@@ -74,8 +74,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||||
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
@@ -95,11 +93,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<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://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>
|
|
||||||
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ app.use(async (c, next) => {
|
|||||||
|
|
||||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||||
const data = c.req.valid("json");
|
const data = c.req.valid("json");
|
||||||
queue.add(data, { groupName: data.serverId });
|
const res = queue.add(data, { groupName: data.serverId });
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
message: "Deployment Added",
|
message: "Deployment Added",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (error) {
|
||||||
if (job.applicationType === "application") {
|
if (job.applicationType === "application") {
|
||||||
await updateApplicationStatus(job.applicationId, "error");
|
await updateApplicationStatus(job.applicationId, "error");
|
||||||
} else if (job.applicationType === "compose") {
|
} else if (job.applicationType === "compose") {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToAllConfigs } from "@dokploy/server";
|
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|||||||
@@ -293,6 +293,29 @@ networks:
|
|||||||
dokploy-network:
|
dokploy-network:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const expectedComposeFile7 = `
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:latest
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
driver: bridge
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.driver.mtu: 1200
|
||||||
|
|
||||||
|
backend:
|
||||||
|
driver: bridge
|
||||||
|
attachable: true
|
||||||
|
|
||||||
|
external_network:
|
||||||
|
external: true
|
||||||
|
name: dokploy-network
|
||||||
|
`;
|
||||||
test("It shoudn't add suffix to dokploy-network", () => {
|
test("It shoudn't add suffix to dokploy-network", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = load(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { addSuffixToAllVolumes } from "@dokploy/server";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
|
import {
|
||||||
|
addSuffixToAllVolumes,
|
||||||
|
addSuffixToVolumesInServices,
|
||||||
|
} from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const baseApp: ApplicationNested = {
|
|||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
organizationId: "",
|
adminId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { FileConfig, User } from "@dokploy/server";
|
import type { Admin, FileConfig } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
@@ -13,34 +13,10 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: User = {
|
const baseAdmin: Admin = {
|
||||||
enablePaidFeatures: false,
|
createdAt: "",
|
||||||
metricsConfig: {
|
authId: "",
|
||||||
containers: {
|
adminId: "string",
|
||||||
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: new Date(),
|
|
||||||
serverIp: null,
|
serverIp: null,
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
host: null,
|
host: null,
|
||||||
@@ -51,19 +27,6 @@ const baseAdmin: User = {
|
|||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
banExpires: new Date(),
|
|
||||||
banned: true,
|
|
||||||
banReason: "",
|
|
||||||
email: "",
|
|
||||||
expirationDate: "",
|
|
||||||
id: "",
|
|
||||||
isRegistered: false,
|
|
||||||
name: "",
|
|
||||||
createdAt2: new Date().toISOString(),
|
|
||||||
emailVerified: false,
|
|
||||||
image: "",
|
|
||||||
updatedAt: new Date(),
|
|
||||||
twoFactorEnabled: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -114,6 +77,8 @@ test("Should not touch config without host", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Should remove websecure if https rollback to http", () => {
|
test("Should remove websecure if https rollback to http", () => {
|
||||||
|
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||||
|
|
||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
{ ...baseAdmin, certificateType: "letsencrypt" },
|
{ ...baseAdmin, certificateType: "letsencrypt" },
|
||||||
"example.com",
|
"example.com",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
|
|||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
organizationId: "",
|
adminId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
|||||||
132
apps/dokploy/components/auth/login-2fa.tsx
Normal file
132
apps/dokploy/components/auth/login-2fa.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
|
||||||
|
import { CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
InputOTP,
|
||||||
|
InputOTPGroup,
|
||||||
|
InputOTPSeparator,
|
||||||
|
InputOTPSlot,
|
||||||
|
} from "@/components/ui/input-otp";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const Login2FASchema = z.object({
|
||||||
|
pin: z.string().min(6, {
|
||||||
|
message: "Pin is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Login2FA = z.infer<typeof Login2FASchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
authId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Login2FA = ({ authId }: Props) => {
|
||||||
|
const { push } = useRouter();
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, isError, error } =
|
||||||
|
api.auth.verifyLogin2FA.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<Login2FA>({
|
||||||
|
defaultValues: {
|
||||||
|
pin: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(Login2FASchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
pin: "",
|
||||||
|
});
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: Login2FA) => {
|
||||||
|
await mutateAsync({
|
||||||
|
pin: data.pin,
|
||||||
|
id: authId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Signin successfully", {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
push("/dashboard/projects");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Signin failed", {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
<CardTitle className="text-xl font-bold">2FA Login</CardTitle>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="pin"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col max-sm:items-center">
|
||||||
|
<FormLabel>Pin</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex">
|
||||||
|
<InputOTP
|
||||||
|
maxLength={6}
|
||||||
|
{...field}
|
||||||
|
pattern={REGEXP_ONLY_DIGITS}
|
||||||
|
>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} className="border-border" />
|
||||||
|
<InputOTPSlot index={1} className="border-border" />
|
||||||
|
<InputOTPSlot index={2} className="border-border" />
|
||||||
|
<InputOTPSlot index={3} className="border-border" />
|
||||||
|
<InputOTPSlot index={4} className="border-border" />
|
||||||
|
<InputOTPSlot index={5} className="border-border" />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Please enter the 6 digits code provided by your authenticator
|
||||||
|
app.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Submit 2FA
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str);
|
return JSON.parse(str);
|
||||||
} catch (_e) {
|
} catch (e) {
|
||||||
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||||
return z.NEVER;
|
return z.NEVER;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { api } from "@/utils/api";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -17,6 +17,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 React from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} 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, Trash2 } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HandlePorts } from "./handle-ports";
|
import { HandlePorts } from "./handle-ports";
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} 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, Trash2 } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HandleRedirect } from "./handle-redirect";
|
import { HandleRedirect } from "./handle-redirect";
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} 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, Trash2 } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HandleSecurity } from "./handle-security";
|
import { HandleSecurity } from "./handle-security";
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -144,6 +144,38 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
className="grid w-full gap-8 "
|
className="grid w-full gap-8 "
|
||||||
>
|
>
|
||||||
<div className="grid w-full md:grid-cols-2 gap-4">
|
<div className="grid w-full md:grid-cols-2 gap-4">
|
||||||
|
<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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="memoryLimit"
|
name="memoryLimit"
|
||||||
@@ -177,37 +209,6 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { File, Loader2 } from "lucide-react";
|
import { File, Loader2 } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} 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, Trash2 } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { ServiceType } from "../show-resources";
|
import type { ServiceType } from "../show-resources";
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
@@ -44,8 +45,8 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
<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 postgres database use the
|
||||||
to setup the volumes
|
following config to setup the volumes
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Content</span>
|
<span className="font-medium">Content</span>
|
||||||
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
|
<span className="text-sm text-muted-foreground">
|
||||||
{mount.content}
|
{mount.content}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,21 +113,12 @@ 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
|
||||||
|
|||||||
@@ -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 } from "lucide-react";
|
import { PenBoxIcon, 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";
|
||||||
@@ -77,7 +77,7 @@ export const UpdateVolume = ({
|
|||||||
serviceType,
|
serviceType,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const _utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data } = api.mounts.one.useQuery(
|
const { data } = api.mounts.one.useQuery(
|
||||||
{
|
{
|
||||||
mountId,
|
mountId,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -17,15 +17,8 @@ interface Props {
|
|||||||
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 [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
@@ -106,8 +99,6 @@ export const ShowDeployment = ({
|
|||||||
}
|
}
|
||||||
}, [filteredLogs, autoScroll]);
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
const optionalErrors = parseLogs(errorMessage || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -166,17 +157,9 @@ export const ShowDeployment = ({
|
|||||||
<TerminalLine key={index} log={log} noTimestamp />
|
<TerminalLine key={index} log={log} noTimestamp />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
{optionalErrors.length > 0 ? (
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
optionalErrors.map((log: LogLine, index: number) => (
|
</div>
|
||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -73,66 +74,60 @@ 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>
|
<DialogAction
|
||||||
<DialogAction
|
title="Delete Domain"
|
||||||
title="Delete Domain"
|
description="Are you sure you want to delete this domain?"
|
||||||
description="Are you sure you want to delete this domain?"
|
type="destructive"
|
||||||
type="destructive"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await deleteDomain({
|
||||||
await deleteDomain({
|
domainId: item.domainId,
|
||||||
domainId: item.domainId,
|
})
|
||||||
|
.then((data) => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Domain deleted successfully");
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
refetch();
|
toast.error("Error deleting domain");
|
||||||
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}
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { Toggle } from "@/components/ui/toggle";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
import { type CSSProperties, useEffect, useState } from "react";
|
import React, { type CSSProperties, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Form } from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
import { Secrets } from "@/components/ui/secrets";
|
import { Secrets } from "@/components/ui/secrets";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
data: repositories,
|
data: repositories,
|
||||||
isLoading: isLoadingRepositories,
|
isLoading: isLoadingRepositories,
|
||||||
error,
|
error,
|
||||||
|
isError,
|
||||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||||
{
|
{
|
||||||
bitbucketId,
|
bitbucketId,
|
||||||
@@ -234,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", {
|
||||||
@@ -244,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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { GitBranch, UploadCloud } from "lucide-react";
|
import { GitBranch, LockIcon, UploadCloud } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
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";
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -27,7 +28,8 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.application.stop.useMutation();
|
api.application.stop.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
const { mutateAsync: deploy, isLoading: isDeploying } =
|
||||||
|
api.application.deploy.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.application.reload.useMutation();
|
api.application.reload.useMutation();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -25,9 +26,7 @@ export const ShowPreviewBuilds = ({
|
|||||||
serverId,
|
serverId,
|
||||||
trigger,
|
trigger,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
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}>
|
||||||
@@ -78,7 +77,7 @@ export const ShowPreviewBuilds = ({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveLog(deployment);
|
setActiveLog(deployment.logPath);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@@ -90,10 +89,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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
RocketIcon,
|
RocketIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
import { AddPreviewDomain } from "./add-preview-domain";
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="env"
|
name="env"
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Secrets
|
<Secrets
|
||||||
|
|||||||
@@ -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 { PenBoxIcon } from "lucide-react";
|
import { AlertTriangle, PenBoxIcon, 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";
|
||||||
|
|||||||
@@ -19,6 +19,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 React from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { ServiceType } from "@dokploy/server/db/schema";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { Copy, Trash2 } from "lucide-react";
|
import { Copy, Trash2 } from "lucide-react";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -40,42 +39,16 @@ const deleteComposeSchema = z.object({
|
|||||||
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
composeId: string;
|
||||||
type: ServiceType | "application";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteService = ({ id, type }: Props) => {
|
export const DeleteCompose = ({ composeId }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
|
||||||
const queryMap = {
|
const { data } = api.compose.one.useQuery(
|
||||||
postgres: () =>
|
{ composeId },
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
{ enabled: !!composeId },
|
||||||
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 } = 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 { push } = useRouter();
|
||||||
const form = useForm<DeleteCompose>({
|
const form = useForm<DeleteCompose>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -89,23 +62,14 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
if (formData.projectName === expectedName) {
|
if (formData.projectName === expectedName) {
|
||||||
const { deleteVolumes } = formData;
|
const { deleteVolumes } = formData;
|
||||||
await mutateAsync({
|
await mutateAsync({ composeId, deleteVolumes })
|
||||||
mongoId: id || "",
|
|
||||||
postgresId: id || "",
|
|
||||||
redisId: id || "",
|
|
||||||
mysqlId: id || "",
|
|
||||||
mariadbId: id || "",
|
|
||||||
applicationId: id || "",
|
|
||||||
composeId: id || "",
|
|
||||||
deleteVolumes,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
push(`/dashboard/project/${result?.projectId}`);
|
push(`/dashboard/project/${result?.projectId}`);
|
||||||
toast.success("deleted successfully");
|
toast.success("Compose deleted successfully");
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error deleting the service");
|
toast.error("Error deleting the compose");
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
form.setError("projectName", {
|
form.setError("projectName", {
|
||||||
@@ -131,8 +95,8 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will permanently delete the
|
||||||
service. If you are sure please enter the service name to delete
|
compose. If you are sure please enter the compose name to delete
|
||||||
this service.
|
this compose.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
@@ -155,7 +119,9 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (data?.name && data?.appName) {
|
if (data?.name && data?.appName) {
|
||||||
copy(`${data.name}/${data.appName}`);
|
navigator.clipboard.writeText(
|
||||||
|
`${data.name}/${data.appName}`,
|
||||||
|
);
|
||||||
toast.success("Copied to clipboard. Be careful!");
|
toast.success("Copied to clipboard. Be careful!");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -176,29 +142,27 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{type === "compose" && (
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="deleteVolumes"
|
||||||
name="deleteVolumes"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<FormControl>
|
||||||
<FormControl>
|
<Checkbox
|
||||||
<Checkbox
|
checked={field.value}
|
||||||
checked={field.value}
|
onCheckedChange={field.onChange}
|
||||||
onCheckedChange={field.onChange}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormLabel className="ml-2">
|
<FormLabel className="ml-2">
|
||||||
Delete volumes associated with this compose
|
Delete volumes associated with this compose
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ interface Props {
|
|||||||
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 [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
@@ -107,8 +105,6 @@ export const ShowDeploymentCompose = ({
|
|||||||
}
|
}
|
||||||
}, [filteredLogs, autoScroll]);
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
const optionalErrors = parseLogs(errorMessage || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -165,17 +161,9 @@ export const ShowDeploymentCompose = ({
|
|||||||
<TerminalLine key={index} log={log} noTimestamp />
|
<TerminalLine key={index} log={log} noTimestamp />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
{optionalErrors.length > 0 ? (
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
optionalErrors.map((log: LogLine, index: number) => (
|
</div>
|
||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -73,70 +74,59 @@ 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>
|
||||||
|
<DialogAction
|
||||||
<div className="flex gap-8">
|
title="Delete Domain"
|
||||||
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
|
description="Are you sure you want to delete this domain?"
|
||||||
<span>{item.path}</span>
|
type="destructive"
|
||||||
<span>{item.port}</span>
|
onClick={async () => {
|
||||||
<span>{item.https ? "HTTPS" : "HTTP"}</span>
|
await deleteDomain({
|
||||||
</div>
|
domainId: item.domainId,
|
||||||
|
})
|
||||||
<div className="flex gap-2">
|
.then((data) => {
|
||||||
<AddDomainCompose
|
refetch();
|
||||||
composeId={composeId}
|
toast.success("Domain deleted successfully");
|
||||||
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) => {
|
.catch(() => {
|
||||||
refetch();
|
toast.error("Error deleting domain");
|
||||||
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}
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -35,7 +35,8 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
{ enabled: !!composeId },
|
{ enabled: !!composeId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.compose.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddComposeFile>({
|
const form = useForm<AddComposeFile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -75,7 +76,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((_e) => {
|
.catch((e) => {
|
||||||
toast.error("Error updating the Compose config");
|
toast.error("Error updating the Compose config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -124,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"
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
data: repositories,
|
data: repositories,
|
||||||
isLoading: isLoadingRepositories,
|
isLoading: isLoadingRepositories,
|
||||||
error,
|
error,
|
||||||
|
isError,
|
||||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||||
{
|
{
|
||||||
bitbucketId,
|
bitbucketId,
|
||||||
@@ -236,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", {
|
||||||
@@ -246,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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { CodeIcon, GitBranch } from "lucide-react";
|
import { CodeIcon, GitBranch, LockIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ComposeFileEditor } from "../compose-file-editor";
|
import { ComposeFileEditor } from "../compose-file-editor";
|
||||||
|
|||||||
@@ -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,10 +20,15 @@ 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";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle, Dices } 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";
|
||||||
@@ -39,7 +48,7 @@ type Schema = z.infer<typeof schema>;
|
|||||||
export const RandomizeCompose = ({ composeId }: Props) => {
|
export const RandomizeCompose = ({ composeId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [compose, setCompose] = useState<string>("");
|
const [compose, setCompose] = useState<string>("");
|
||||||
const [_isOpen, _setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, error, isError } =
|
const { mutateAsync, error, isError } =
|
||||||
api.compose.randomizeCompose.useMutation();
|
api.compose.randomizeCompose.useMutation();
|
||||||
|
|
||||||
@@ -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 || "",
|
||||||
@@ -76,7 +84,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
suffix: formData?.suffix || "",
|
suffix: formData?.suffix || "",
|
||||||
randomize: formData?.randomize || false,
|
randomize: formData?.randomize || false,
|
||||||
})
|
})
|
||||||
.then(async (_data) => {
|
.then(async (data) => {
|
||||||
randomizeCompose();
|
randomizeCompose();
|
||||||
refetch();
|
refetch();
|
||||||
toast.success("Compose updated");
|
toast.success("Compose updated");
|
||||||
@@ -102,117 +110,126 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch((_err) => {});
|
.catch((err) => {});
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import React from "react";
|
||||||
import { ComposeActions } from "./actions";
|
import { ComposeActions } from "./actions";
|
||||||
import { ShowProviderFormCompose } from "./generic/show";
|
import { ShowProviderFormCompose } from "./generic/show";
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader, Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
export const DockerLogs = dynamic(
|
export const DockerLogs = dynamic(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -22,22 +20,18 @@ import { api } from "@/utils/api";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ContainerPaidMonitoring } from "./show-paid-container-monitoring";
|
import { DockerMonitoring } from "../../monitoring/docker/show";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
appType: "stack" | "docker-compose";
|
appType: "stack" | "docker-compose";
|
||||||
baseUrl: string;
|
|
||||||
token: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComposePaidMonitoring = ({
|
export const ShowMonitoringCompose = ({
|
||||||
appName,
|
appName,
|
||||||
appType = "stack",
|
appType = "stack",
|
||||||
serverId,
|
serverId,
|
||||||
baseUrl,
|
|
||||||
token,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
@@ -50,9 +44,9 @@ export const ComposePaidMonitoring = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [containerAppName, setContainerAppName] = useState<string | undefined>(
|
const [containerAppName, setContainerAppName] = useState<
|
||||||
"",
|
string | undefined
|
||||||
);
|
>();
|
||||||
|
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
|
|
||||||
@@ -68,7 +62,7 @@ export const ComposePaidMonitoring = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card className="bg-background border-0">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Monitoring</CardTitle>
|
<CardTitle className="text-xl">Monitoring</CardTitle>
|
||||||
<CardDescription>Watch the usage of your compose</CardDescription>
|
<CardDescription>Watch the usage of your compose</CardDescription>
|
||||||
@@ -104,9 +98,7 @@ export const ComposePaidMonitoring = ({
|
|||||||
value={container.name}
|
value={container.name}
|
||||||
>
|
>
|
||||||
{container.name} ({container.containerId}){" "}
|
{container.name} ({container.containerId}){" "}
|
||||||
<Badge variant={badgeStateColor(container.state)}>
|
{container.state}
|
||||||
{container.state}
|
|
||||||
</Badge>
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||||
@@ -126,13 +118,10 @@ export const ComposePaidMonitoring = ({
|
|||||||
Restart
|
Restart
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<DockerMonitoring
|
||||||
<ContainerPaidMonitoring
|
appName={containerAppName || ""}
|
||||||
appName={containerAppName || ""}
|
appType={appType}
|
||||||
baseUrl={baseUrl}
|
/>
|
||||||
token={token}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { AddBackup } from "./add-backup";
|
import { AddBackup } from "./add-backup";
|
||||||
@@ -74,7 +75,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup it is required to set at least 1 provider.
|
To create a backup it is required to set at least 1 provider.
|
||||||
Please, go to{" "}
|
Please, go to{" "}
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, PenBoxIcon, 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";
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { FancyAnsi } from "fancy-ansi";
|
import { FancyAnsi } from "fancy-ansi";
|
||||||
import { escapeRegExp } from "lodash";
|
import { escapeRegExp } from "lodash";
|
||||||
|
import React from "react";
|
||||||
import { type LogLine, getLogType } from "./utils";
|
import { type LogLine, getLogType } from "./utils";
|
||||||
|
|
||||||
interface LogLineProps {
|
interface LogLineProps {
|
||||||
@@ -47,12 +48,23 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const htmlContent = fancyAnsi.toHtml(text);
|
const htmlContent = fancyAnsi.toHtml(text);
|
||||||
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
|
||||||
|
|
||||||
const modifiedContent = htmlContent.replace(
|
const modifiedContent = htmlContent.replace(
|
||||||
searchRegex,
|
/<span([^>]*)>([^<]*)<\/span>/g,
|
||||||
(match) =>
|
(match, attrs, content) => {
|
||||||
`<span class="bg-orange-200/80 dark:bg-orange-900/80 font-bold">${match}</span>`,
|
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
||||||
|
if (!content.match(searchRegex)) return match;
|
||||||
|
|
||||||
|
const segments = content.split(searchRegex);
|
||||||
|
const wrappedSegments = segments
|
||||||
|
.map((segment: string) =>
|
||||||
|
segment.toLowerCase() === term.toLowerCase()
|
||||||
|
? `<span${attrs} class="bg-yellow-200/50 dark:bg-yellow-900/50">${segment}</span>`
|
||||||
|
: segment,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `<span${attrs}>${wrappedSegments}</span>`;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const LOG_STYLES: Record<LogType, LogStyle> = {
|
|||||||
|
|
||||||
export function parseLogs(logString: string): LogLine[] {
|
export function parseLogs(logString: string): LogLine[] {
|
||||||
// Regex to match the log line format
|
// Regex to match the log line format
|
||||||
// Example of return :
|
// Exemple of return :
|
||||||
// 1 2024-12-10T10:00:00.000Z The server is running on port 8080
|
// 1 2024-12-10T10:00:00.000Z The server is running on port 8080
|
||||||
// Should return :
|
// Should return :
|
||||||
// { timestamp: new Date("2024-12-10T10:00:00.000Z"),
|
// { timestamp: new Date("2024-12-10T10:00:00.000Z"),
|
||||||
@@ -63,10 +63,18 @@ export function parseLogs(logString: string): LogLine[] {
|
|||||||
|
|
||||||
if (!message?.trim()) return null;
|
if (!message?.trim()) return null;
|
||||||
|
|
||||||
|
// Delete other timestamps and keep only the one from --timestamps
|
||||||
|
const cleanedMessage = message
|
||||||
|
?.replace(
|
||||||
|
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rawTimestamp: timestamp ?? null,
|
rawTimestamp: timestamp ?? null,
|
||||||
timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
|
timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
|
||||||
message: message.trim(),
|
message: cleanedMessage,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((log) => log !== null);
|
.filter((log) => log !== null);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
import {
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type SortingState,
|
||||||
|
type VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { ChevronDown, Container } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -22,19 +37,6 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import {
|
|
||||||
type ColumnFiltersState,
|
|
||||||
type SortingState,
|
|
||||||
type VisibilityState,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getFilteredRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import { ChevronDown, Container } from "lucide-react";
|
|
||||||
import * as React from "react";
|
|
||||||
import { columns } from "./colums";
|
import { columns } from "./colums";
|
||||||
export type Container = NonNullable<
|
export type Container = NonNullable<
|
||||||
RouterOutputs["docker"]["getContainers"]
|
RouterOutputs["docker"]["getContainers"]
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Tree } from "@/components/ui/file-tree";
|
import { Tree } from "@/components/ui/file-tree";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
import { FileIcon, Folder, Link, Loader2, Workflow } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ShowTraefikFile } from "./show-traefik-file";
|
import { ShowTraefikFile } from "./show-traefik-file";
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
|
|||||||
@@ -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 { PenBoxIcon } from "lucide-react";
|
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["block"];
|
acummulativeData: DockerStatsJSON["block"];
|
||||||
@@ -90,11 +90,9 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
|||||||
if (active && payload && payload.length && payload[0]) {
|
if (active && payload && payload.length && payload[0]) {
|
||||||
return (
|
return (
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
||||||
{payload[0].payload.time && (
|
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
<p>{`Read ${payload[0].payload.readMb.toFixed(2)} MB`}</p>
|
||||||
)}
|
<p>{`Write: ${payload[0].payload.writeMb.toFixed(3)} MB`}</p>
|
||||||
<p>{`Read ${payload[0].payload.readMb} `}</p>
|
|
||||||
<p>{`Write: ${payload[0].payload.writeMb} `}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["cpu"];
|
acummulativeData: DockerStatsJSON["cpu"];
|
||||||
@@ -19,7 +19,7 @@ export const DockerCpuChart = ({ acummulativeData }: Props) => {
|
|||||||
return {
|
return {
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
time: item.time,
|
time: item.time,
|
||||||
usage: item.value.toString().split("%")[0],
|
usage: item.value.toFixed(2),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
@@ -75,9 +75,7 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
|||||||
if (active && payload && payload.length && payload[0]) {
|
if (active && payload && payload.length && payload[0]) {
|
||||||
return (
|
return (
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
||||||
{payload[0].payload.time && (
|
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
|
||||||
)}
|
|
||||||
<p>{`CPU Usage: ${payload[0].payload.usage}%`}</p>
|
<p>{`CPU Usage: ${payload[0].payload.usage}%`}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["disk"];
|
acummulativeData: DockerStatsJSON["disk"];
|
||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show";
|
||||||
import { convertMemoryToBytes } from "./show-free-container-monitoring";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["memory"];
|
acummulativeData: DockerStatsJSON["memory"];
|
||||||
memoryLimitGB: number;
|
memoryLimitGB: number;
|
||||||
@@ -23,8 +23,7 @@ export const DockerMemoryChart = ({
|
|||||||
return {
|
return {
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
// @ts-ignore
|
usage: (item.value.used / 1024 ** 3).toFixed(2),
|
||||||
usage: (convertMemoryToBytes(item.value.used) / 1024 ** 3).toFixed(2),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
@@ -76,13 +75,10 @@ interface CustomTooltipProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
||||||
if (active && payload && payload.length && payload[0] && payload[0].payload) {
|
if (active && payload && payload.length && payload[0]) {
|
||||||
return (
|
return (
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
||||||
{payload[0].payload.time && (
|
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p>{`Memory usage: ${payload[0].payload.usage} GB`}</p>
|
<p>{`Memory usage: ${payload[0].payload.usage} GB`}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show";
|
||||||
|
1;
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["network"];
|
acummulativeData: DockerStatsJSON["network"];
|
||||||
}
|
}
|
||||||
@@ -18,8 +19,8 @@ export const DockerNetworkChart = ({ acummulativeData }: Props) => {
|
|||||||
return {
|
return {
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
inMB: item.value.inputMb,
|
inMB: item.value.inputMb.toFixed(2),
|
||||||
outMB: item.value.outputMb,
|
outMB: item.value.outputMb.toFixed(2),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
@@ -85,11 +86,9 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
|||||||
if (active && payload && payload.length && payload[0]) {
|
if (active && payload && payload.length && payload[0]) {
|
||||||
return (
|
return (
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
||||||
{payload[0].payload.time && (
|
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
<p>{`In MB Usage: ${payload[0].payload.inMB} MB`}</p>
|
||||||
)}
|
<p>{`Out MB Usage: ${payload[0].payload.outMB} MB`}</p>
|
||||||
<p>{`In Usage: ${payload[0].payload.inMB} `}</p>
|
|
||||||
<p>{`Out Usage: ${payload[0].payload.outMB} `}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
314
apps/dokploy/components/dashboard/monitoring/docker/show.tsx
Normal file
314
apps/dokploy/components/dashboard/monitoring/docker/show.tsx
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { DockerBlockChart } from "./docker-block-chart";
|
||||||
|
import { DockerCpuChart } from "./docker-cpu-chart";
|
||||||
|
import { DockerDiskChart } from "./docker-disk-chart";
|
||||||
|
import { DockerMemoryChart } from "./docker-memory-chart";
|
||||||
|
import { DockerNetworkChart } from "./docker-network-chart";
|
||||||
|
|
||||||
|
const defaultData = {
|
||||||
|
cpu: {
|
||||||
|
value: 0,
|
||||||
|
time: "",
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
value: {
|
||||||
|
used: 0,
|
||||||
|
free: 0,
|
||||||
|
usedPercentage: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
time: "",
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
value: {
|
||||||
|
readMb: 0,
|
||||||
|
writeMb: 0,
|
||||||
|
},
|
||||||
|
time: "",
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
value: {
|
||||||
|
inputMb: 0,
|
||||||
|
outputMb: 0,
|
||||||
|
},
|
||||||
|
time: "",
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
value: { diskTotal: 0, diskUsage: 0, diskUsedPercentage: 0, diskFree: 0 },
|
||||||
|
time: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
appName: string;
|
||||||
|
appType?: "application" | "stack" | "docker-compose";
|
||||||
|
}
|
||||||
|
export interface DockerStats {
|
||||||
|
cpu: {
|
||||||
|
value: number;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
memory: {
|
||||||
|
value: {
|
||||||
|
used: number;
|
||||||
|
free: number;
|
||||||
|
usedPercentage: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
block: {
|
||||||
|
value: {
|
||||||
|
readMb: number;
|
||||||
|
writeMb: number;
|
||||||
|
};
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
network: {
|
||||||
|
value: {
|
||||||
|
inputMb: number;
|
||||||
|
outputMb: number;
|
||||||
|
};
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
disk: {
|
||||||
|
value: {
|
||||||
|
diskTotal: number;
|
||||||
|
diskUsage: number;
|
||||||
|
diskUsedPercentage: number;
|
||||||
|
diskFree: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DockerStatsJSON = {
|
||||||
|
cpu: DockerStats["cpu"][];
|
||||||
|
memory: DockerStats["memory"][];
|
||||||
|
block: DockerStats["block"][];
|
||||||
|
network: DockerStats["network"][];
|
||||||
|
disk: DockerStats["disk"][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DockerMonitoring = ({
|
||||||
|
appName,
|
||||||
|
appType = "application",
|
||||||
|
}: Props) => {
|
||||||
|
const { data } = api.application.readAppMonitoring.useQuery(
|
||||||
|
{ appName },
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const [acummulativeData, setAcummulativeData] = useState<DockerStatsJSON>({
|
||||||
|
cpu: [],
|
||||||
|
memory: [],
|
||||||
|
block: [],
|
||||||
|
network: [],
|
||||||
|
disk: [],
|
||||||
|
});
|
||||||
|
const [currentData, setCurrentData] = useState<DockerStats>(defaultData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentData(defaultData);
|
||||||
|
|
||||||
|
setAcummulativeData({
|
||||||
|
cpu: [],
|
||||||
|
memory: [],
|
||||||
|
block: [],
|
||||||
|
network: [],
|
||||||
|
disk: [],
|
||||||
|
});
|
||||||
|
}, [appName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
setCurrentData({
|
||||||
|
cpu: data.cpu[data.cpu.length - 1] ?? currentData.cpu,
|
||||||
|
memory: data.memory[data.memory.length - 1] ?? currentData.memory,
|
||||||
|
block: data.block[data.block.length - 1] ?? currentData.block,
|
||||||
|
network: data.network[data.network.length - 1] ?? currentData.network,
|
||||||
|
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
|
||||||
|
});
|
||||||
|
setAcummulativeData({
|
||||||
|
block: data?.block || [],
|
||||||
|
cpu: data?.cpu || [],
|
||||||
|
disk: data?.disk || [],
|
||||||
|
memory: data?.memory || [],
|
||||||
|
network: data?.network || [],
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/listen-docker-stats-monitoring?appName=${appName}&appType=${appType}`;
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
const value = JSON.parse(e.data);
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
cpu: value.data.cpu ?? currentData.cpu,
|
||||||
|
memory: value.data.memory ?? currentData.memory,
|
||||||
|
block: value.data.block ?? currentData.block,
|
||||||
|
disk: value.data.disk ?? currentData.disk,
|
||||||
|
network: value.data.network ?? currentData.network,
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentData(data);
|
||||||
|
|
||||||
|
setAcummulativeData((prevData) => ({
|
||||||
|
cpu: [...prevData.cpu, data.cpu],
|
||||||
|
memory: [...prevData.memory, data.memory],
|
||||||
|
block: [...prevData.block, data.block],
|
||||||
|
network: [...prevData.network, data.network],
|
||||||
|
disk: [...prevData.disk, data.disk],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (e) => {
|
||||||
|
console.log(e.reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => ws.close();
|
||||||
|
}, [appName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||||
|
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
|
||||||
|
<header className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Monitoring
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Watch the usage of your server in the current app
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Used: {currentData.cpu.value.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
<Progress
|
||||||
|
value={currentData.cpu.value}
|
||||||
|
className="w-[100%]"
|
||||||
|
/>
|
||||||
|
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Memory Usage
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
|
||||||
|
</span>
|
||||||
|
<Progress
|
||||||
|
value={currentData.memory.value.usedPercentage}
|
||||||
|
className="w-[100%]"
|
||||||
|
/>
|
||||||
|
<DockerMemoryChart
|
||||||
|
acummulativeData={acummulativeData.memory}
|
||||||
|
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{appName === "dokploy" && (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Disk Space
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
|
||||||
|
</span>
|
||||||
|
<Progress
|
||||||
|
value={currentData.disk.value.diskUsedPercentage}
|
||||||
|
className="w-[100%]"
|
||||||
|
/>
|
||||||
|
<DockerDiskChart
|
||||||
|
acummulativeData={acummulativeData.disk}
|
||||||
|
diskTotal={currentData.disk.value.diskTotal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Block I/O</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{`Read: ${currentData.block.value.readMb.toFixed(
|
||||||
|
2,
|
||||||
|
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
|
||||||
|
3,
|
||||||
|
)} MB`}
|
||||||
|
</span>
|
||||||
|
<DockerBlockChart acummulativeData={acummulativeData.block} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Network I/O
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{`In MB: ${currentData.network.value.inputMb.toFixed(
|
||||||
|
2,
|
||||||
|
)} MB / Out MB: ${currentData.network.value.outputMb.toFixed(
|
||||||
|
2,
|
||||||
|
)} MB`}
|
||||||
|
</span>
|
||||||
|
<DockerNetworkChart
|
||||||
|
acummulativeData={acummulativeData.network}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ContainerFreeMonitoring } from "./show-free-container-monitoring";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
appName: string;
|
|
||||||
serverId?: string;
|
|
||||||
appType: "stack" | "docker-compose";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ComposeFreeMonitoring = ({
|
|
||||||
appName,
|
|
||||||
appType = "stack",
|
|
||||||
serverId,
|
|
||||||
}: Props) => {
|
|
||||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
|
||||||
{
|
|
||||||
appName: appName,
|
|
||||||
appType,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!appName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [containerAppName, setContainerAppName] = useState<
|
|
||||||
string | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
|
||||||
|
|
||||||
const { mutateAsync: restart, isLoading: isRestarting } =
|
|
||||||
api.docker.restartContainer.useMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data && data?.length > 0) {
|
|
||||||
setContainerAppName(data[0]?.name);
|
|
||||||
setContainerId(data[0]?.containerId);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-xl">Monitoring</CardTitle>
|
|
||||||
<CardDescription>Watch the usage of your compose</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<Label>Select a container to watch the monitoring</Label>
|
|
||||||
<div className="flex flex-row gap-4">
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setContainerAppName(value);
|
|
||||||
setContainerId(
|
|
||||||
data?.find((container) => container.name === value)
|
|
||||||
?.containerId,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
value={containerAppName}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
|
||||||
<span>Loading...</span>
|
|
||||||
<Loader2 className="animate-spin size-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<SelectValue placeholder="Select a container" />
|
|
||||||
)}
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
{data?.map((container) => (
|
|
||||||
<SelectItem
|
|
||||||
key={container.containerId}
|
|
||||||
value={container.name}
|
|
||||||
>
|
|
||||||
{container.name} ({container.containerId}){" "}
|
|
||||||
<Badge variant={badgeStateColor(container.state)}>
|
|
||||||
{container.state}
|
|
||||||
</Badge>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button
|
|
||||||
isLoading={isRestarting}
|
|
||||||
onClick={async () => {
|
|
||||||
if (!containerId) return;
|
|
||||||
toast.success(`Restarting container ${containerAppName}`);
|
|
||||||
await restart({ containerId }).then(() => {
|
|
||||||
toast.success("Container restarted");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Restart
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<ContainerFreeMonitoring
|
|
||||||
appName={containerAppName || ""}
|
|
||||||
appType={appType}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { DockerBlockChart } from "./docker-block-chart";
|
|
||||||
import { DockerCpuChart } from "./docker-cpu-chart";
|
|
||||||
import { DockerDiskChart } from "./docker-disk-chart";
|
|
||||||
import { DockerMemoryChart } from "./docker-memory-chart";
|
|
||||||
import { DockerNetworkChart } from "./docker-network-chart";
|
|
||||||
|
|
||||||
const defaultData = {
|
|
||||||
cpu: {
|
|
||||||
value: 0,
|
|
||||||
time: "",
|
|
||||||
},
|
|
||||||
memory: {
|
|
||||||
value: {
|
|
||||||
used: 0,
|
|
||||||
total: 0,
|
|
||||||
},
|
|
||||||
time: "",
|
|
||||||
},
|
|
||||||
block: {
|
|
||||||
value: {
|
|
||||||
readMb: 0,
|
|
||||||
writeMb: 0,
|
|
||||||
},
|
|
||||||
time: "",
|
|
||||||
},
|
|
||||||
network: {
|
|
||||||
value: {
|
|
||||||
inputMb: 0,
|
|
||||||
outputMb: 0,
|
|
||||||
},
|
|
||||||
time: "",
|
|
||||||
},
|
|
||||||
disk: {
|
|
||||||
value: { diskTotal: 0, diskUsage: 0, diskUsedPercentage: 0, diskFree: 0 },
|
|
||||||
time: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
appName: string;
|
|
||||||
appType?: "application" | "stack" | "docker-compose";
|
|
||||||
}
|
|
||||||
export interface DockerStats {
|
|
||||||
cpu: {
|
|
||||||
value: number;
|
|
||||||
time: string;
|
|
||||||
};
|
|
||||||
memory: {
|
|
||||||
value: {
|
|
||||||
used: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
time: string;
|
|
||||||
};
|
|
||||||
block: {
|
|
||||||
value: {
|
|
||||||
readMb: number;
|
|
||||||
writeMb: number;
|
|
||||||
};
|
|
||||||
time: string;
|
|
||||||
};
|
|
||||||
network: {
|
|
||||||
value: {
|
|
||||||
inputMb: number;
|
|
||||||
outputMb: number;
|
|
||||||
};
|
|
||||||
time: string;
|
|
||||||
};
|
|
||||||
disk: {
|
|
||||||
value: {
|
|
||||||
diskTotal: number;
|
|
||||||
diskUsage: number;
|
|
||||||
diskUsedPercentage: number;
|
|
||||||
diskFree: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
time: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DockerStatsJSON = {
|
|
||||||
cpu: DockerStats["cpu"][];
|
|
||||||
memory: DockerStats["memory"][];
|
|
||||||
block: DockerStats["block"][];
|
|
||||||
network: DockerStats["network"][];
|
|
||||||
disk: DockerStats["disk"][];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertMemoryToBytes = (
|
|
||||||
memoryString: string | undefined,
|
|
||||||
): number => {
|
|
||||||
if (!memoryString || typeof memoryString !== "string") {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = Number.parseFloat(memoryString) || 0;
|
|
||||||
const unit = memoryString.replace(/[0-9.]/g, "").trim();
|
|
||||||
|
|
||||||
switch (unit) {
|
|
||||||
case "KiB":
|
|
||||||
return value * 1024;
|
|
||||||
case "MiB":
|
|
||||||
return value * 1024 * 1024;
|
|
||||||
case "GiB":
|
|
||||||
return value * 1024 * 1024 * 1024;
|
|
||||||
case "TiB":
|
|
||||||
return value * 1024 * 1024 * 1024 * 1024;
|
|
||||||
default:
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContainerFreeMonitoring = ({
|
|
||||||
appName,
|
|
||||||
appType = "application",
|
|
||||||
}: Props) => {
|
|
||||||
const { data } = api.application.readAppMonitoring.useQuery(
|
|
||||||
{ appName },
|
|
||||||
{
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const [acummulativeData, setAcummulativeData] = useState<DockerStatsJSON>({
|
|
||||||
cpu: [],
|
|
||||||
memory: [],
|
|
||||||
block: [],
|
|
||||||
network: [],
|
|
||||||
disk: [],
|
|
||||||
});
|
|
||||||
const [currentData, setCurrentData] = useState<DockerStats>(defaultData);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentData(defaultData);
|
|
||||||
|
|
||||||
setAcummulativeData({
|
|
||||||
cpu: [],
|
|
||||||
memory: [],
|
|
||||||
block: [],
|
|
||||||
network: [],
|
|
||||||
disk: [],
|
|
||||||
});
|
|
||||||
}, [appName]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
setCurrentData({
|
|
||||||
cpu: data.cpu[data.cpu.length - 1] ?? currentData.cpu,
|
|
||||||
memory: data.memory[data.memory.length - 1] ?? currentData.memory,
|
|
||||||
block: data.block[data.block.length - 1] ?? currentData.block,
|
|
||||||
network: data.network[data.network.length - 1] ?? currentData.network,
|
|
||||||
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
|
|
||||||
});
|
|
||||||
setAcummulativeData({
|
|
||||||
block: data?.block || [],
|
|
||||||
cpu: data?.cpu || [],
|
|
||||||
disk: data?.disk || [],
|
|
||||||
memory: data?.memory || [],
|
|
||||||
network: data?.network || [],
|
|
||||||
});
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/listen-docker-stats-monitoring?appName=${appName}&appType=${appType}`;
|
|
||||||
const ws = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
|
||||||
const value = JSON.parse(e.data);
|
|
||||||
if (!value) return;
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
cpu: value.data.cpu ?? currentData.cpu,
|
|
||||||
memory: value.data.memory ?? currentData.memory,
|
|
||||||
block: value.data.block ?? currentData.block,
|
|
||||||
disk: value.data.disk ?? currentData.disk,
|
|
||||||
network: value.data.network ?? currentData.network,
|
|
||||||
};
|
|
||||||
|
|
||||||
setCurrentData(data);
|
|
||||||
|
|
||||||
setAcummulativeData((prevData) => ({
|
|
||||||
cpu: [...prevData.cpu, data.cpu],
|
|
||||||
memory: [...prevData.memory, data.memory],
|
|
||||||
block: [...prevData.block, data.block],
|
|
||||||
network: [...prevData.network, data.network],
|
|
||||||
disk: [...prevData.disk, data.disk],
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = (e) => {
|
|
||||||
console.log(e.reason);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => ws.close();
|
|
||||||
}, [appName]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-background shadow-md flex flex-col gap-4">
|
|
||||||
<header className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Watch the usage of your server in the current app
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Used: {currentData.cpu.value}%
|
|
||||||
</span>
|
|
||||||
<Progress value={currentData.cpu.value} className="w-[100%]" />
|
|
||||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Memory Usage</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{`Used: ${currentData.memory.value.used} / Limit: ${currentData.memory.value.total} `}
|
|
||||||
</span>
|
|
||||||
<Progress
|
|
||||||
value={
|
|
||||||
// @ts-ignore
|
|
||||||
(convertMemoryToBytes(currentData.memory.value.used) /
|
|
||||||
// @ts-ignore
|
|
||||||
convertMemoryToBytes(currentData.memory.value.total)) *
|
|
||||||
100
|
|
||||||
}
|
|
||||||
className="w-[100%]"
|
|
||||||
/>
|
|
||||||
<DockerMemoryChart
|
|
||||||
acummulativeData={acummulativeData.memory}
|
|
||||||
memoryLimitGB={
|
|
||||||
// @ts-ignore
|
|
||||||
convertMemoryToBytes(currentData.memory.value.total) /
|
|
||||||
1024 ** 3
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
{appName === "dokploy" && (
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Disk Space</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
|
|
||||||
</span>
|
|
||||||
<Progress
|
|
||||||
value={currentData.disk.value.diskUsedPercentage}
|
|
||||||
className="w-[100%]"
|
|
||||||
/>
|
|
||||||
<DockerDiskChart
|
|
||||||
acummulativeData={acummulativeData.disk}
|
|
||||||
diskTotal={currentData.disk.value.diskTotal}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Block I/O</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
|
|
||||||
</span>
|
|
||||||
<DockerBlockChart acummulativeData={acummulativeData.block} />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Network I/O</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
|
|
||||||
</span>
|
|
||||||
<DockerNetworkChart acummulativeData={acummulativeData.network} />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
type ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
} from "@/components/ui/chart";
|
|
||||||
import { formatTimestamp } from "@/lib/utils";
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
interface ContainerMetric {
|
|
||||||
timestamp: string;
|
|
||||||
BlockIO: {
|
|
||||||
read: number;
|
|
||||||
write: number;
|
|
||||||
readUnit: string;
|
|
||||||
writeUnit: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: ContainerMetric[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
read: {
|
|
||||||
label: "Read",
|
|
||||||
color: "hsl(217, 91%, 60%)", // Azul brillante
|
|
||||||
},
|
|
||||||
write: {
|
|
||||||
label: "Write",
|
|
||||||
color: "hsl(142, 71%, 45%)", // Verde brillante
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export const ContainerBlockChart = ({ data }: Props) => {
|
|
||||||
const formattedData = data.map((metric) => ({
|
|
||||||
timestamp: metric.timestamp,
|
|
||||||
read: metric.BlockIO.read,
|
|
||||||
write: metric.BlockIO.write,
|
|
||||||
readUnit: metric.BlockIO.readUnit,
|
|
||||||
writeUnit: metric.BlockIO.writeUnit,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const latestData = formattedData[formattedData.length - 1] || {
|
|
||||||
timestamp: "",
|
|
||||||
read: 0,
|
|
||||||
write: 0,
|
|
||||||
readUnit: "B",
|
|
||||||
writeUnit: "B",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-transparent">
|
|
||||||
<CardHeader className="border-b py-5">
|
|
||||||
<CardTitle>Block I/O</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Read: {latestData.read}
|
|
||||||
{latestData.readUnit} / Write: {latestData.write}
|
|
||||||
{latestData.writeUnit}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="aspect-auto h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<AreaChart data={formattedData}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="fillRead" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(217, 91%, 60%)"
|
|
||||||
stopOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(217, 91%, 60%)"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="fillWrite" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(142, 71%, 45%)"
|
|
||||||
stopOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(142, 71%, 45%)"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="timestamp"
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
minTickGap={32}
|
|
||||||
tickFormatter={(value) => formatTimestamp(value)}
|
|
||||||
/>
|
|
||||||
<YAxis />
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={({ active, payload, label }) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
const data = payload?.[0]?.payload;
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Time
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{formatTimestamp(label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Read
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{data.read}
|
|
||||||
{data.readUnit}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Write
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{data.write}
|
|
||||||
{data.writeUnit}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="Write"
|
|
||||||
dataKey="write"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillWrite)"
|
|
||||||
stroke="hsl(142, 71%, 45%)"
|
|
||||||
strokeWidth={2}
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="Read"
|
|
||||||
dataKey="read"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillRead)"
|
|
||||||
stroke="hsl(217, 91%, 60%)"
|
|
||||||
strokeWidth={2}
|
|
||||||
fillOpacity={0.3}
|
|
||||||
/>
|
|
||||||
<ChartLegend
|
|
||||||
content={<ChartLegendContent />}
|
|
||||||
verticalAlign="bottom"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
type ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
} from "@/components/ui/chart";
|
|
||||||
import { formatTimestamp } from "@/lib/utils";
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
interface ContainerMetric {
|
|
||||||
timestamp: string;
|
|
||||||
CPU: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: ContainerMetric[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
cpu: {
|
|
||||||
label: "CPU",
|
|
||||||
color: "hsl(var(--chart-1))",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export const ContainerCPUChart = ({ data }: Props) => {
|
|
||||||
const formattedData = data.map((metric) => ({
|
|
||||||
timestamp: metric.timestamp,
|
|
||||||
cpu: metric.CPU,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const latestData = formattedData[formattedData.length - 1] || {
|
|
||||||
timestamp: "",
|
|
||||||
cpu: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-transparent">
|
|
||||||
<CardHeader className="border-b py-5">
|
|
||||||
<CardTitle>CPU</CardTitle>
|
|
||||||
<CardDescription>CPU Usage: {latestData.cpu}%</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="aspect-auto h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<AreaChart data={formattedData}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(var(--chart-1))"
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(var(--chart-1))"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="timestamp"
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
minTickGap={32}
|
|
||||||
tickFormatter={(value) => formatTimestamp(value)}
|
|
||||||
/>
|
|
||||||
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={({ active, payload, label }) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
const data = payload?.[0]?.payload;
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Time
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{formatTimestamp(label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
CPU
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">{data.cpu}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="CPU"
|
|
||||||
dataKey="cpu"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillCPU)"
|
|
||||||
stroke="hsl(var(--chart-1))"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<ChartLegend
|
|
||||||
content={<ChartLegendContent />}
|
|
||||||
verticalAlign="bottom"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
type ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
} from "@/components/ui/chart";
|
|
||||||
import { formatTimestamp } from "@/lib/utils";
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
interface ContainerMetric {
|
|
||||||
timestamp: string;
|
|
||||||
Memory: {
|
|
||||||
percentage: number;
|
|
||||||
used: number;
|
|
||||||
total: number;
|
|
||||||
usedUnit: string;
|
|
||||||
totalUnit: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: ContainerMetric[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
memory: {
|
|
||||||
label: "Memory",
|
|
||||||
color: "hsl(var(--chart-2))",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
const formatMemoryValue = (value: number) => {
|
|
||||||
return value.toLocaleString("en-US", {
|
|
||||||
minimumFractionDigits: 1,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContainerMemoryChart = ({ data }: Props) => {
|
|
||||||
const formattedData = data.map((metric) => ({
|
|
||||||
timestamp: metric.timestamp,
|
|
||||||
memory: metric.Memory.percentage,
|
|
||||||
usage: `${formatMemoryValue(metric.Memory.used)}${metric.Memory.usedUnit} / ${formatMemoryValue(metric.Memory.total)}${metric.Memory.totalUnit}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const latestData = formattedData[formattedData.length - 1] || {
|
|
||||||
timestamp: "",
|
|
||||||
memory: 0,
|
|
||||||
usage: "0 / 0 B",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-transparent">
|
|
||||||
<CardHeader className="border-b py-5">
|
|
||||||
<CardTitle>Memory</CardTitle>
|
|
||||||
<CardDescription>Memory Usage: {latestData.usage}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="aspect-auto h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<AreaChart data={formattedData}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(var(--chart-2))"
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(var(--chart-2))"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="timestamp"
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
minTickGap={32}
|
|
||||||
tickFormatter={(value) => formatTimestamp(value)}
|
|
||||||
/>
|
|
||||||
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={({ active, payload, label }) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
const data = payload?.[0]?.payload;
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Time
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{formatTimestamp(label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Memory
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">{data.memory}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col col-span-2">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Usage
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">{data.usage}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="Memory"
|
|
||||||
dataKey="memory"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillMemory)"
|
|
||||||
stroke="hsl(var(--chart-2))"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<ChartLegend
|
|
||||||
content={<ChartLegendContent />}
|
|
||||||
verticalAlign="bottom"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
type ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
} from "@/components/ui/chart";
|
|
||||||
import { formatTimestamp } from "@/lib/utils";
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
interface ContainerMetric {
|
|
||||||
timestamp: string;
|
|
||||||
Network: {
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
inputUnit: string;
|
|
||||||
outputUnit: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: ContainerMetric[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FormattedMetric {
|
|
||||||
timestamp: string;
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
inputUnit: string;
|
|
||||||
outputUnit: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
input: {
|
|
||||||
label: "Input",
|
|
||||||
color: "hsl(var(--chart-3))",
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
label: "Output",
|
|
||||||
color: "hsl(var(--chart-4))",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export const ContainerNetworkChart = ({ data }: Props) => {
|
|
||||||
const formattedData: FormattedMetric[] = data.map((metric) => ({
|
|
||||||
timestamp: metric.timestamp,
|
|
||||||
input: metric.Network.input,
|
|
||||||
output: metric.Network.output,
|
|
||||||
inputUnit: metric.Network.inputUnit,
|
|
||||||
outputUnit: metric.Network.outputUnit,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const latestData = formattedData[formattedData.length - 1] || {
|
|
||||||
input: 0,
|
|
||||||
output: 0,
|
|
||||||
inputUnit: "B",
|
|
||||||
outputUnit: "B",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-transparent">
|
|
||||||
<CardHeader className="border-b py-5">
|
|
||||||
<CardTitle>Network I/O</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Input: {latestData.input}
|
|
||||||
{latestData.inputUnit} / Output: {latestData.output}
|
|
||||||
{latestData.outputUnit}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="aspect-auto h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<AreaChart data={formattedData}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="fillInput" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(var(--chart-3))"
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(var(--chart-3))"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="fillOutput" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(var(--chart-4))"
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(var(--chart-4))"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="timestamp"
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
minTickGap={32}
|
|
||||||
tickFormatter={(value) => formatTimestamp(value)}
|
|
||||||
/>
|
|
||||||
<YAxis />
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={({ active, payload, label }) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
const data = payload?.[0]?.payload;
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Time
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{formatTimestamp(label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Input
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{data.input}
|
|
||||||
{data.inputUnit}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Output
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{data.output}
|
|
||||||
{data.outputUnit}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="Input"
|
|
||||||
dataKey="input"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillInput)"
|
|
||||||
stroke="hsl(var(--chart-3))"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="Output"
|
|
||||||
dataKey="output"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillOutput)"
|
|
||||||
stroke="hsl(var(--chart-4))"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<ChartLegend
|
|
||||||
content={<ChartLegendContent />}
|
|
||||||
verticalAlign="bottom"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { Cpu, HardDrive, Loader2, MemoryStick, Network } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ContainerBlockChart } from "./container-block-chart";
|
|
||||||
import { ContainerCPUChart } from "./container-cpu-chart";
|
|
||||||
import { ContainerMemoryChart } from "./container-memory-chart";
|
|
||||||
import { ContainerNetworkChart } from "./container-network-chart";
|
|
||||||
|
|
||||||
const REFRESH_INTERVALS = {
|
|
||||||
"5000": "5 Seconds",
|
|
||||||
"10000": "10 Seconds",
|
|
||||||
"20000": "20 Seconds",
|
|
||||||
"30000": "30 Seconds",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const DATA_POINTS_OPTIONS = {
|
|
||||||
"50": "50 points",
|
|
||||||
"200": "200 points",
|
|
||||||
"500": "500 points",
|
|
||||||
"800": "800 points",
|
|
||||||
"1200": "1200 points",
|
|
||||||
"1600": "1600 points",
|
|
||||||
"2000": "2000 points",
|
|
||||||
all: "All points",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
interface ContainerMetric {
|
|
||||||
timestamp: string;
|
|
||||||
CPU: number;
|
|
||||||
Memory: {
|
|
||||||
percentage: number;
|
|
||||||
used: number;
|
|
||||||
total: number;
|
|
||||||
unit: string;
|
|
||||||
usedUnit: string;
|
|
||||||
totalUnit: string;
|
|
||||||
};
|
|
||||||
Network: {
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
inputUnit: string;
|
|
||||||
outputUnit: string;
|
|
||||||
};
|
|
||||||
BlockIO: {
|
|
||||||
read: number;
|
|
||||||
write: number;
|
|
||||||
readUnit: string;
|
|
||||||
writeUnit: string;
|
|
||||||
};
|
|
||||||
Container: string;
|
|
||||||
ID: string;
|
|
||||||
Name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
appName: string;
|
|
||||||
baseUrl: string;
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
|
||||||
const [historicalData, setHistoricalData] = useState<ContainerMetric[]>([]);
|
|
||||||
const [metrics, setMetrics] = useState<ContainerMetric>(
|
|
||||||
{} as ContainerMetric,
|
|
||||||
);
|
|
||||||
const [dataPoints, setDataPoints] =
|
|
||||||
useState<keyof typeof DATA_POINTS_OPTIONS>("50");
|
|
||||||
const [refreshInterval, setRefreshInterval] = useState<string>("5000");
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
error: queryError,
|
|
||||||
} = api.user.getContainerMetrics.useQuery(
|
|
||||||
{
|
|
||||||
url: baseUrl,
|
|
||||||
token,
|
|
||||||
dataPoints,
|
|
||||||
appName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refetchInterval:
|
|
||||||
dataPoints === "all" ? undefined : Number.parseInt(refreshInterval),
|
|
||||||
enabled: !!appName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
setHistoricalData(data);
|
|
||||||
// @ts-ignore
|
|
||||||
setMetrics(data[data.length - 1]);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-[400px] w-full items-center justify-center">
|
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryError) {
|
|
||||||
return (
|
|
||||||
<div className="mt-5 flex min-h-[55vh] w-full items-center justify-center p-4">
|
|
||||||
<div className="max-w-xl text-center">
|
|
||||||
<p className="mb-2 text-base font-medium leading-none text-muted-foreground">
|
|
||||||
Error fetching metrics for{" "}
|
|
||||||
<strong className="text-primary">{appName}</strong>
|
|
||||||
</p>
|
|
||||||
<p className="whitespace-pre-line text-sm text-destructive">
|
|
||||||
{queryError instanceof Error
|
|
||||||
? queryError.message
|
|
||||||
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
|
|
||||||
</p>
|
|
||||||
<p className=" text-sm text-muted-foreground">URL: {baseUrl}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
|
||||||
<h2 className="text-2xl font-bold tracking-tight">
|
|
||||||
Container Monitoring
|
|
||||||
</h2>
|
|
||||||
<div className="flex items-center gap-4 flex-wrap">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-muted-foreground">Data points:</span>
|
|
||||||
<Select
|
|
||||||
value={dataPoints}
|
|
||||||
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
|
|
||||||
setDataPoints(value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue placeholder="Select points" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
|
|
||||||
<SelectItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Refresh interval:
|
|
||||||
</span>
|
|
||||||
<Select
|
|
||||||
value={refreshInterval}
|
|
||||||
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
|
|
||||||
setRefreshInterval(value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue placeholder="Select interval" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
|
|
||||||
<SelectItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Cards */}
|
|
||||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<Card className="p-6 bg-transparent">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="text-sm font-medium">CPU Usage</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-2xl font-bold">{metrics.CPU}%</p>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6 bg-transparent">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="text-sm font-medium">Memory Usage</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-2xl font-bold">
|
|
||||||
{metrics?.Memory?.percentage}%
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
|
||||||
{metrics?.Memory?.used} {metrics?.Memory?.unit} /{" "}
|
|
||||||
{metrics?.Memory?.total} {metrics?.Memory?.unit}
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6 bg-transparent">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Network className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="text-sm font-medium">Network I/O</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-2xl font-bold">
|
|
||||||
{metrics?.Network?.input} {metrics?.Network?.inputUnit} /{" "}
|
|
||||||
{metrics?.Network?.output} {metrics?.Network?.outputUnit}
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-6 bg-transparent">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="text-sm font-medium">Block I/O</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-2xl font-bold">
|
|
||||||
{metrics?.BlockIO?.read} {metrics?.BlockIO?.readUnit} /{" "}
|
|
||||||
{metrics?.BlockIO?.write} {metrics?.BlockIO?.writeUnit}
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Container Information */}
|
|
||||||
<Card className="p-6 bg-transparent">
|
|
||||||
<h3 className="text-lg font-medium mb-4">Container Information</h3>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-muted-foreground">
|
|
||||||
Container ID
|
|
||||||
</h4>
|
|
||||||
<p className="mt-1">{metrics.ID}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-muted-foreground">Name</h4>
|
|
||||||
<p className="mt-1 truncate">{metrics.Name}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Charts Grid */}
|
|
||||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-1 xl:grid-cols-2">
|
|
||||||
<ContainerCPUChart data={historicalData} />
|
|
||||||
<ContainerMemoryChart data={historicalData} />
|
|
||||||
<ContainerBlockChart data={historicalData} />
|
|
||||||
<ContainerNetworkChart data={historicalData} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
type ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
} from "@/components/ui/chart";
|
|
||||||
import { formatTimestamp } from "@/lib/utils";
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
interface CPUChartProps {
|
|
||||||
data: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
cpu: {
|
|
||||||
label: "CPU",
|
|
||||||
color: "hsl(var(--chart-1))",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export function CPUChart({ data }: CPUChartProps) {
|
|
||||||
const latestData = data[data.length - 1] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-transparent">
|
|
||||||
<CardHeader className="border-b py-5">
|
|
||||||
<CardTitle>CPU</CardTitle>
|
|
||||||
<CardDescription>CPU Usage: {latestData.cpu}%</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="aspect-auto h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<AreaChart data={data}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(var(--chart-1))"
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(var(--chart-1))"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="timestamp"
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
minTickGap={32}
|
|
||||||
tickFormatter={(value) => formatTimestamp(value)}
|
|
||||||
/>
|
|
||||||
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={({ active, payload, label }) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
const data = payload?.[0]?.payload;
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Time
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{formatTimestamp(label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
CPU
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">{data.cpu}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="CPU"
|
|
||||||
dataKey="cpu"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillCPU)"
|
|
||||||
stroke="hsl(var(--chart-1))"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<ChartLegend
|
|
||||||
content={<ChartLegendContent />}
|
|
||||||
verticalAlign="bottom"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import { HardDrive } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Label,
|
|
||||||
PolarGrid,
|
|
||||||
PolarRadiusAxis,
|
|
||||||
RadialBar,
|
|
||||||
RadialBarChart,
|
|
||||||
} from "recharts";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
|
|
||||||
|
|
||||||
interface RadialChartProps {
|
|
||||||
data: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DiskChart({ data }: RadialChartProps) {
|
|
||||||
const diskUsed = Number.parseFloat(data.diskUsed || 0);
|
|
||||||
const totalDiskGB = Number.parseFloat(data.totalDisk || 0);
|
|
||||||
const usedDiskGB = (totalDiskGB * diskUsed) / 100;
|
|
||||||
|
|
||||||
const chartData = [
|
|
||||||
{
|
|
||||||
disk: 25,
|
|
||||||
fill: "hsl(var(--chart-2))",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
disk: {
|
|
||||||
label: "Disk",
|
|
||||||
color: "hsl(var(--chart-2))",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
const endAngle = (diskUsed * 360) / 100;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="flex flex-col bg-transparent">
|
|
||||||
<CardHeader className="items-center border-b pb-5">
|
|
||||||
<CardTitle>Disk</CardTitle>
|
|
||||||
<CardDescription>Storage Space</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex-1 pb-0">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="mx-auto aspect-square max-h-[250px]"
|
|
||||||
>
|
|
||||||
<RadialBarChart
|
|
||||||
data={chartData}
|
|
||||||
startAngle={0}
|
|
||||||
endAngle={endAngle}
|
|
||||||
innerRadius={80}
|
|
||||||
outerRadius={110}
|
|
||||||
>
|
|
||||||
<PolarGrid
|
|
||||||
gridType="circle"
|
|
||||||
radialLines={false}
|
|
||||||
stroke="none"
|
|
||||||
className="first:fill-muted last:fill-background"
|
|
||||||
polarRadius={[86, 74]}
|
|
||||||
/>
|
|
||||||
<RadialBar
|
|
||||||
dataKey="disk"
|
|
||||||
background
|
|
||||||
cornerRadius={10}
|
|
||||||
fill="hsl(var(--chart-2))"
|
|
||||||
/>
|
|
||||||
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
|
|
||||||
<Label
|
|
||||||
content={({ viewBox }) => {
|
|
||||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
x={viewBox.cx}
|
|
||||||
y={viewBox.cy}
|
|
||||||
textAnchor="middle"
|
|
||||||
dominantBaseline="middle"
|
|
||||||
>
|
|
||||||
<tspan
|
|
||||||
x={viewBox.cx}
|
|
||||||
y={viewBox.cy}
|
|
||||||
className="fill-foreground text-4xl font-bold"
|
|
||||||
>
|
|
||||||
{diskUsed.toFixed(1)}%
|
|
||||||
</tspan>
|
|
||||||
<tspan
|
|
||||||
x={viewBox.cx}
|
|
||||||
y={(viewBox.cy || 0) + 24}
|
|
||||||
className="fill-muted-foreground text-sm"
|
|
||||||
>
|
|
||||||
Used
|
|
||||||
</tspan>
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PolarRadiusAxis>
|
|
||||||
</RadialBarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex-col gap-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2 font-medium leading-none">
|
|
||||||
<HardDrive className="h-4 w-4" /> {usedDiskGB.toFixed(1)} GB used
|
|
||||||
</div>
|
|
||||||
<div className="leading-none text-muted-foreground">
|
|
||||||
Of {totalDiskGB.toFixed(1)} GB total
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
type ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
} from "@/components/ui/chart";
|
|
||||||
import { formatTimestamp } from "@/lib/utils";
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
interface MemoryChartProps {
|
|
||||||
data: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
Memory: {
|
|
||||||
label: "Memory",
|
|
||||||
color: "hsl(var(--chart-2))",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export function MemoryChart({ data }: MemoryChartProps) {
|
|
||||||
const latestData = data[data.length - 1] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-transparent">
|
|
||||||
<CardHeader className="border-b py-5">
|
|
||||||
<CardTitle>Memory</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Memory Usage: {latestData.memUsedGB} GB of {latestData.memTotal} GB (
|
|
||||||
{latestData.memUsed}%)
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="aspect-auto h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<AreaChart data={data}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(var(--chart-2))"
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(var(--chart-2))"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="timestamp"
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
minTickGap={32}
|
|
||||||
tickFormatter={(value) => formatTimestamp(value)}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
yAxisId="left"
|
|
||||||
orientation="left"
|
|
||||||
tickFormatter={(value) => `${value}%`}
|
|
||||||
domain={[0, 100]}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
yAxisId="right"
|
|
||||||
orientation="right"
|
|
||||||
tickFormatter={(value) => `${value.toFixed(1)} GB`}
|
|
||||||
domain={[
|
|
||||||
0,
|
|
||||||
Math.ceil(Number.parseFloat(latestData.memTotal || "0")),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={({ active, payload, label }) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
const data = payload?.[0]?.payload;
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Time
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{formatTimestamp(label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Memory
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{data.memUsed}% ({data.memUsedGB} GB)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
yAxisId="left"
|
|
||||||
dataKey="memUsed"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillMemory)"
|
|
||||||
stroke="hsl(var(--chart-2))"
|
|
||||||
strokeWidth={2}
|
|
||||||
name="Memory"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
type ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
} from "@/components/ui/chart";
|
|
||||||
import { formatTimestamp } from "@/lib/utils";
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
interface NetworkChartProps {
|
|
||||||
data: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
networkIn: {
|
|
||||||
label: "Network In",
|
|
||||||
color: "hsl(var(--chart-3))",
|
|
||||||
},
|
|
||||||
networkOut: {
|
|
||||||
label: "Network Out",
|
|
||||||
color: "hsl(var(--chart-4))",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export function NetworkChart({ data }: NetworkChartProps) {
|
|
||||||
const latestData = data[data.length - 1] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-transparent">
|
|
||||||
<CardHeader className="border-b py-5">
|
|
||||||
<CardTitle>Network</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Network Traffic: ↑ {latestData.networkOut} KB/s ↓{" "}
|
|
||||||
{latestData.networkIn} KB/s
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
|
||||||
<ChartContainer
|
|
||||||
config={chartConfig}
|
|
||||||
className="aspect-auto h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<AreaChart data={data}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="fillNetworkIn" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(var(--chart-3))"
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(var(--chart-3))"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="fillNetworkOut" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop
|
|
||||||
offset="5%"
|
|
||||||
stopColor="hsl(var(--chart-4))"
|
|
||||||
stopOpacity={0.8}
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="95%"
|
|
||||||
stopColor="hsl(var(--chart-4))"
|
|
||||||
stopOpacity={0.1}
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="timestamp"
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
minTickGap={32}
|
|
||||||
tickFormatter={(value) => formatTimestamp(value)}
|
|
||||||
/>
|
|
||||||
<YAxis tickFormatter={(value) => `${value} KB/s`} />
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={({ active, payload, label }) => {
|
|
||||||
if (active && payload && payload.length) {
|
|
||||||
const data = payload?.[0]?.payload;
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Time
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{formatTimestamp(label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
|
||||||
Network
|
|
||||||
</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
↑ {data.networkOut} KB/s
|
|
||||||
<br />↓ {data.networkIn} KB/s
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="Network In"
|
|
||||||
dataKey="networkIn"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillNetworkIn)"
|
|
||||||
stroke="hsl(var(--chart-3))"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
name="Network Out"
|
|
||||||
dataKey="networkOut"
|
|
||||||
type="monotone"
|
|
||||||
fill="url(#fillNetworkOut)"
|
|
||||||
stroke="hsl(var(--chart-4))"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<ChartLegend
|
|
||||||
content={<ChartLegendContent />}
|
|
||||||
verticalAlign="bottom"
|
|
||||||
align="center"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { CPUChart } from "./cpu-chart";
|
|
||||||
import { DiskChart } from "./disk-chart";
|
|
||||||
import { MemoryChart } from "./memory-chart";
|
|
||||||
import { NetworkChart } from "./network-chart";
|
|
||||||
|
|
||||||
const REFRESH_INTERVALS = {
|
|
||||||
"5000": "5 Seconds",
|
|
||||||
"10000": "10 Seconds",
|
|
||||||
"20000": "20 Seconds",
|
|
||||||
"30000": "30 Seconds",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const DATA_POINTS_OPTIONS = {
|
|
||||||
"50": "50 points",
|
|
||||||
"200": "200 points",
|
|
||||||
"500": "500 points",
|
|
||||||
"800": "800 points",
|
|
||||||
"1200": "1200 points",
|
|
||||||
"1600": "1600 points",
|
|
||||||
"2000": "2000 points",
|
|
||||||
all: "All points",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
interface SystemMetrics {
|
|
||||||
cpu: string;
|
|
||||||
cpuModel: string;
|
|
||||||
cpuCores: number;
|
|
||||||
cpuPhysicalCores: number;
|
|
||||||
cpuSpeed: number;
|
|
||||||
os: string;
|
|
||||||
distro: string;
|
|
||||||
kernel: string;
|
|
||||||
arch: string;
|
|
||||||
memUsed: string;
|
|
||||||
memUsedGB: string;
|
|
||||||
memTotal: string;
|
|
||||||
uptime: number;
|
|
||||||
diskUsed: string;
|
|
||||||
totalDisk: string;
|
|
||||||
networkIn: string;
|
|
||||||
networkOut: string;
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
BASE_URL?: string;
|
|
||||||
token?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowPaidMonitoring = ({
|
|
||||||
BASE_URL = process.env.NEXT_PUBLIC_METRICS_URL ||
|
|
||||||
"http://localhost:3001/metrics",
|
|
||||||
token = process.env.NEXT_PUBLIC_METRICS_TOKEN || "my-token",
|
|
||||||
}: Props) => {
|
|
||||||
const [historicalData, setHistoricalData] = useState<SystemMetrics[]>([]);
|
|
||||||
const [metrics, setMetrics] = useState<SystemMetrics>({} as SystemMetrics);
|
|
||||||
const [dataPoints, setDataPoints] =
|
|
||||||
useState<keyof typeof DATA_POINTS_OPTIONS>("50");
|
|
||||||
const [refreshInterval, setRefreshInterval] = useState<string>("5000");
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
error: queryError,
|
|
||||||
} = api.server.getServerMetrics.useQuery(
|
|
||||||
{
|
|
||||||
url: BASE_URL,
|
|
||||||
token,
|
|
||||||
dataPoints,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refetchInterval:
|
|
||||||
dataPoints === "all" ? undefined : Number.parseInt(refreshInterval),
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
const formattedData = data.map((metric: SystemMetrics) => ({
|
|
||||||
timestamp: metric.timestamp,
|
|
||||||
cpu: Number.parseFloat(metric.cpu),
|
|
||||||
cpuModel: metric.cpuModel,
|
|
||||||
cpuCores: metric.cpuCores,
|
|
||||||
cpuPhysicalCores: metric.cpuPhysicalCores,
|
|
||||||
cpuSpeed: metric.cpuSpeed,
|
|
||||||
os: metric.os,
|
|
||||||
distro: metric.distro,
|
|
||||||
kernel: metric.kernel,
|
|
||||||
arch: metric.arch,
|
|
||||||
memUsed: Number.parseFloat(metric.memUsed),
|
|
||||||
memUsedGB: Number.parseFloat(metric.memUsedGB),
|
|
||||||
memTotal: Number.parseFloat(metric.memTotal),
|
|
||||||
networkIn: Number.parseFloat(metric.networkIn),
|
|
||||||
networkOut: Number.parseFloat(metric.networkOut),
|
|
||||||
diskUsed: Number.parseFloat(metric.diskUsed),
|
|
||||||
totalDisk: Number.parseFloat(metric.totalDisk),
|
|
||||||
uptime: metric.uptime,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
setHistoricalData(formattedData);
|
|
||||||
// @ts-ignore
|
|
||||||
setMetrics(formattedData[formattedData.length - 1] || {});
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const formatUptime = (seconds: number): string => {
|
|
||||||
const days = Math.floor(seconds / (24 * 60 * 60));
|
|
||||||
const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60));
|
|
||||||
const minutes = Math.floor((seconds % (60 * 60)) / 60);
|
|
||||||
|
|
||||||
return `${days}d ${hours}h ${minutes}m`;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-[400px] w-full items-center justify-center">
|
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryError) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[55vh] w-full items-center justify-center p-4">
|
|
||||||
<div className="max-w-xl text-center">
|
|
||||||
<p className="mb-2 text-base font-medium leading-none text-muted-foreground">
|
|
||||||
Error fetching metrics{" "}
|
|
||||||
</p>
|
|
||||||
<p className="whitespace-pre-line text-sm text-destructive">
|
|
||||||
{queryError instanceof Error
|
|
||||||
? queryError.message
|
|
||||||
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
|
|
||||||
</p>
|
|
||||||
<p className=" text-sm text-muted-foreground">URL: {BASE_URL}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 pt-5 pb-10 w-full md:px-4">
|
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
|
||||||
<h2 className="text-2xl font-bold tracking-tight">System Monitoring</h2>
|
|
||||||
<div className="flex items-center gap-4 flex-wrap">
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-muted-foreground">Data points:</span>
|
|
||||||
<Select
|
|
||||||
value={dataPoints}
|
|
||||||
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
|
|
||||||
setDataPoints(value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue placeholder="Select points" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
|
|
||||||
<SelectItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Refresh interval:
|
|
||||||
</span>
|
|
||||||
<Select
|
|
||||||
value={refreshInterval}
|
|
||||||
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
|
|
||||||
setRefreshInterval(value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue placeholder="Select interval" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
|
|
||||||
<SelectItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Cards */}
|
|
||||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="text-sm font-medium">Uptime</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-2xl font-bold">
|
|
||||||
{formatUptime(metrics.uptime || 0)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="text-sm font-medium">CPU Usage</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-2xl font-bold">{metrics.cpu}%</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border text-card-foreground bg-transparent shadow-sm p-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="text-sm font-medium">Memory Usage</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-2xl font-bold">
|
|
||||||
{metrics.memUsedGB} GB / {metrics.memTotal} GB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<h3 className="text-sm font-medium">Disk Usage</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-2xl font-bold">{metrics.diskUsed}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* System Information */}
|
|
||||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
|
||||||
<h3 className="text-lg font-medium mb-4">System Information</h3>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-muted-foreground">CPU</h4>
|
|
||||||
<p className="mt-1">{metrics.cpuModel}</p>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{metrics.cpuPhysicalCores} Physical Cores ({metrics.cpuCores}{" "}
|
|
||||||
Threads) @ {metrics.cpuSpeed}GHz
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-muted-foreground">
|
|
||||||
Operating System
|
|
||||||
</h4>
|
|
||||||
<p className="mt-1">{metrics.distro}</p>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
Kernel: {metrics.kernel} ({metrics.arch})
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Charts Grid */}
|
|
||||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-1 xl:grid-cols-2">
|
|
||||||
<CPUChart data={historicalData} />
|
|
||||||
<MemoryChart data={historicalData} />
|
|
||||||
<DiskChart data={metrics} />
|
|
||||||
<NetworkChart data={historicalData} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user