mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
349bc89851 | ||
|
|
9f6f872536 | ||
|
|
e378d89477 | ||
|
|
63e7eacae9 | ||
|
|
f4ab588516 | ||
|
|
4d8a0ba58f | ||
|
|
e88cd11041 | ||
|
|
5f174a883b | ||
|
|
536a6ba2ff | ||
|
|
213fa08210 | ||
|
|
d5c6a601d8 | ||
|
|
452793c8e5 | ||
|
|
385fbf4af5 | ||
|
|
3590f3bed2 | ||
|
|
9b2fcaea31 | ||
|
|
5abcc82215 | ||
|
|
ee855452e3 | ||
|
|
d000b526d3 | ||
|
|
9bf88b90c3 | ||
|
|
b1a48d4636 | ||
|
|
c34c4b244e | ||
|
|
bb59a0cd3f | ||
|
|
44e6a117dd | ||
|
|
bfdc73f8d1 | ||
|
|
64ada7020a | ||
|
|
4706adc0c0 | ||
|
|
e01d92d1d9 | ||
|
|
fe22890311 | ||
|
|
2b7c7632f4 | ||
|
|
1b7244e841 |
@@ -11,7 +11,6 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
cp apps/dokploy/.env.production.example .env.production
|
cp apps/dokploy/.env.production.example .env.production
|
||||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Build and push AMD64 image
|
name: Build and push AMD64 image
|
||||||
command: |
|
command: |
|
||||||
@@ -62,7 +61,7 @@ jobs:
|
|||||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||||
echo $VERSION
|
echo $VERSION
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
|
|
||||||
docker manifest create dokploy/dokploy:${TAG} \
|
docker manifest create dokploy/dokploy:${TAG} \
|
||||||
dokploy/dokploy:${TAG}-amd64 \
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
dokploy/dokploy:${TAG}-arm64
|
dokploy/dokploy:${TAG}-arm64
|
||||||
|
|||||||
BIN
.github/sponsors/lxaer.png
vendored
BIN
.github/sponsors/lxaer.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 267 KiB |
73
.github/workflows/deploy.yml
vendored
73
.github/workflows/deploy.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Build Docker images
|
name: Build Docs & Website Docker images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -48,74 +48,3 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: dokploy/website:latest
|
tags: dokploy/website:latest
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
|
||||||
build-and-push-cloud-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.cloud
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
|
||||||
platforms: linux/amd64
|
|
||||||
|
|
||||||
build-and-push-schedule-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.schedule
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
|
||||||
platforms: linux/amd64
|
|
||||||
|
|
||||||
|
|
||||||
build-and-push-server-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile.server
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
|
||||||
platforms: linux/amd64
|
|
||||||
3
.github/workflows/pull-request.yml
vendored
3
.github/workflows/pull-request.yml
vendored
@@ -18,7 +18,6 @@ jobs:
|
|||||||
node-version: 18.18.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
|
||||||
- run: pnpm biome ci
|
- run: pnpm biome ci
|
||||||
- run: pnpm typecheck
|
- run: pnpm typecheck
|
||||||
|
|
||||||
@@ -33,7 +32,6 @@ jobs:
|
|||||||
node-version: 18.18.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
|
||||||
- run: pnpm build
|
- run: pnpm build
|
||||||
|
|
||||||
parallel-tests:
|
parallel-tests:
|
||||||
@@ -46,5 +44,4 @@ jobs:
|
|||||||
node-version: 18.18.0
|
node-version: 18.18.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
|
||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
npx commitlint --edit "$1"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Skip Husky install in production and CI
|
|
||||||
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
const husky = (await import("husky")).default;
|
|
||||||
console.log(husky());
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pnpm run check
|
|
||||||
git add .
|
|
||||||
@@ -71,12 +71,6 @@ Run the command that will spin up all the required services and files.
|
|||||||
pnpm run dokploy:setup
|
pnpm run dokploy:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the server package (If you make any changes after in the packages/server folder, you need to rebuild and run this command)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run server:build
|
|
||||||
```
|
|
||||||
|
|
||||||
Now run the development server.
|
Now run the development server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|||||||
# Deploy only the dokploy app
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN pnpm --filter=@dokploy/server build
|
|
||||||
RUN pnpm --filter=./apps/dokploy run build
|
RUN pnpm --filter=./apps/dokploy run build
|
||||||
|
|
||||||
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
||||||
|
|
||||||
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
FROM node:18-slim AS base
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
|
||||||
RUN corepack enable
|
|
||||||
|
|
||||||
FROM base AS build
|
|
||||||
COPY . /usr/src/app
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
|
||||||
|
|
||||||
# Deploy only the dokploy app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
RUN pnpm --filter=@dokploy/server build
|
|
||||||
RUN pnpm --filter=./apps/dokploy run build
|
|
||||||
|
|
||||||
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
|
||||||
|
|
||||||
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
|
||||||
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
|
|
||||||
|
|
||||||
FROM base AS dokploy
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Set production
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy only the necessary files
|
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
|
||||||
COPY --from=build /prod/dokploy/dist ./dist
|
|
||||||
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
|
||||||
COPY --from=build /prod/dokploy/public ./public
|
|
||||||
COPY --from=build /prod/dokploy/package.json ./package.json
|
|
||||||
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
|
||||||
COPY --from=build /prod/dokploy/components.json ./components.json
|
|
||||||
COPY --from=build /prod/dokploy/node_modules ./node_modules
|
|
||||||
|
|
||||||
|
|
||||||
# Install RCLONE
|
|
||||||
RUN curl https://rclone.org/install.sh | bash
|
|
||||||
|
|
||||||
# tsx
|
|
||||||
RUN pnpm install -g tsx
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
CMD [ "pnpm", "start" ]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
FROM node:18-slim AS base
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
|
||||||
RUN corepack enable
|
|
||||||
|
|
||||||
FROM base AS build
|
|
||||||
COPY . /usr/src/app
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
|
||||||
|
|
||||||
# Deploy only the dokploy app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
RUN pnpm --filter=@dokploy/server build
|
|
||||||
RUN pnpm --filter=./apps/schedules run build
|
|
||||||
|
|
||||||
RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
|
|
||||||
|
|
||||||
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
|
|
||||||
|
|
||||||
FROM base AS dokploy
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Set production
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
# Copy only the necessary files
|
|
||||||
COPY --from=build /prod/schedules/dist ./dist
|
|
||||||
COPY --from=build /prod/schedules/package.json ./package.json
|
|
||||||
COPY --from=build /prod/schedules/node_modules ./node_modules
|
|
||||||
|
|
||||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
FROM node:18-slim AS base
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
|
||||||
RUN corepack enable
|
|
||||||
|
|
||||||
FROM base AS build
|
|
||||||
COPY . /usr/src/app
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
|
||||||
|
|
||||||
# Deploy only the dokploy app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
RUN pnpm --filter=@dokploy/server build
|
|
||||||
RUN pnpm --filter=./apps/api run build
|
|
||||||
|
|
||||||
RUN pnpm --filter=./apps/api --prod deploy /prod/api
|
|
||||||
|
|
||||||
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
|
|
||||||
|
|
||||||
FROM base AS dokploy
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Set production
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
# Copy only the necessary files
|
|
||||||
COPY --from=build /prod/api/dist ./dist
|
|
||||||
COPY --from=build /prod/api/package.json ./package.json
|
|
||||||
COPY --from=build /prod/api/node_modules ./node_modules
|
|
||||||
|
|
||||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
|
||||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
## Additional Terms for Specific Features
|
## Additional Terms for Specific Features
|
||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -30,9 +30,8 @@ Dokploy include multiples features to make your life easier.
|
|||||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
||||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||||
- **CLI/API**: Manage your applications and databases using the command line or through the API.
|
- **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
||||||
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
|
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
|
||||||
- **Multi Server**: Deploy and manager your applications remotely to external servers.
|
|
||||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
@@ -59,14 +58,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
### Hero Sponsors 🎖
|
### Hero Sponsors 🎖
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" ><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/></a>
|
||||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block;">
|
|
||||||
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
|
||||||
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
|
|
||||||
@@ -89,7 +81,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
#### Organizations:
|
#### Organizations:
|
||||||
|
|||||||
@@ -1,33 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@dokploy/api",
|
"name": "my-app",
|
||||||
"version": "0.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
"dev": "tsx watch src/index.ts"
|
||||||
"build": "tsc --project tsconfig.json",
|
|
||||||
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
|
||||||
"typecheck": "tsc --noEmit"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pino": "9.4.0",
|
|
||||||
"pino-pretty": "11.2.2",
|
|
||||||
"@hono/zod-validator": "0.3.0",
|
|
||||||
"zod": "^3.23.4",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"@dokploy/server": "workspace:*",
|
|
||||||
"@hono/node-server": "^1.12.1",
|
"@hono/node-server": "^1.12.1",
|
||||||
"hono": "^4.5.8",
|
"hono": "^4.5.8",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1"
|
||||||
"redis": "4.7.0",
|
|
||||||
"@nerimity/mimiqueue": "1.2.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.4.2",
|
|
||||||
"@types/react": "^18.2.37",
|
|
||||||
"@types/react-dom": "^18.2.15",
|
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"tsx": "^4.7.1"
|
"tsx": "^4.7.1"
|
||||||
},
|
}
|
||||||
"packageManager": "pnpm@9.5.0"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,66 @@
|
|||||||
import { serve } from "@hono/node-server";
|
import { serve } from "@hono/node-server";
|
||||||
|
import { config } from "dotenv";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import "dotenv/config";
|
import { cors } from "hono/cors";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { validateLemonSqueezyLicense } from "./utils";
|
||||||
import { Queue } from "@nerimity/mimiqueue";
|
|
||||||
import { createClient } from "redis";
|
config();
|
||||||
import { logger } from "./logger";
|
|
||||||
import { type DeployJob, deployJobSchema } from "./schema";
|
|
||||||
import { deploy } from "./utils";
|
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
const redisClient = createClient({
|
|
||||||
url: process.env.REDIS_URL,
|
app.use(
|
||||||
|
"/*",
|
||||||
|
cors({
|
||||||
|
origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js
|
||||||
|
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allowHeaders: ["Content-Type", "Authorization"],
|
||||||
|
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
|
||||||
|
maxAge: 600,
|
||||||
|
credentials: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
||||||
|
export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
||||||
|
|
||||||
|
app.get("/v1/health", (c) => {
|
||||||
|
return c.text("Hello Hono!");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(async (c, next) => {
|
app.post("/v1/validate-license", async (c) => {
|
||||||
if (c.req.path === "/health") {
|
const { licenseKey } = await c.req.json();
|
||||||
return next();
|
|
||||||
}
|
|
||||||
const authHeader = c.req.header("X-API-Key");
|
|
||||||
|
|
||||||
if (process.env.API_KEY !== authHeader) {
|
if (!licenseKey) {
|
||||||
return c.json({ message: "Invalid API Key" }, 403);
|
return c.json({ error: "License key is required" }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
return next();
|
try {
|
||||||
|
const licenseValidation = await validateLemonSqueezyLicense(licenseKey);
|
||||||
|
|
||||||
|
if (licenseValidation.valid) {
|
||||||
|
return c.json({
|
||||||
|
valid: true,
|
||||||
|
message: "License is valid",
|
||||||
|
metadata: licenseValidation.meta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
valid: false,
|
||||||
|
message: licenseValidation.error || "Invalid license",
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during license validation:", error);
|
||||||
|
return c.json({ error: "Internal server error" }, 500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
const port = 4000;
|
||||||
const data = c.req.valid("json");
|
console.log(`Server is running on port ${port}`);
|
||||||
const res = queue.add(data, { groupName: data.serverId });
|
|
||||||
return c.json(
|
serve({
|
||||||
{
|
fetch: app.fetch,
|
||||||
message: "Deployment Added",
|
port,
|
||||||
},
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/health", async (c) => {
|
|
||||||
return c.json({ status: "ok" });
|
|
||||||
});
|
|
||||||
|
|
||||||
const queue = new Queue({
|
|
||||||
name: "deployments",
|
|
||||||
process: async (job: DeployJob) => {
|
|
||||||
logger.info("Deploying job", job);
|
|
||||||
return await deploy(job);
|
|
||||||
},
|
|
||||||
redisClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await redisClient.connect();
|
|
||||||
await redisClient.flushAll();
|
|
||||||
logger.info("Redis Cleaned");
|
|
||||||
})();
|
|
||||||
|
|
||||||
const port = Number.parseInt(process.env.PORT || "3000");
|
|
||||||
logger.info("Starting Deployments Server ✅", port);
|
|
||||||
serve({ fetch: app.fetch, port });
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import pino from "pino";
|
|
||||||
|
|
||||||
export const logger = pino({
|
|
||||||
transport: {
|
|
||||||
target: "pino-pretty",
|
|
||||||
options: {
|
|
||||||
colorize: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|
||||||
z.object({
|
|
||||||
applicationId: z.string(),
|
|
||||||
titleLog: z.string(),
|
|
||||||
descriptionLog: z.string(),
|
|
||||||
server: z.boolean().optional(),
|
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
|
||||||
applicationType: z.literal("application"),
|
|
||||||
serverId: z.string().min(1),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
composeId: z.string(),
|
|
||||||
titleLog: z.string(),
|
|
||||||
descriptionLog: z.string(),
|
|
||||||
server: z.boolean().optional(),
|
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
|
||||||
applicationType: z.literal("compose"),
|
|
||||||
serverId: z.string().min(1),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
|
||||||
@@ -1,96 +1,28 @@
|
|||||||
import {
|
import { LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_STORE_ID } from ".";
|
||||||
deployApplication,
|
|
||||||
deployCompose,
|
|
||||||
deployRemoteApplication,
|
|
||||||
deployRemoteCompose,
|
|
||||||
rebuildApplication,
|
|
||||||
rebuildCompose,
|
|
||||||
rebuildRemoteApplication,
|
|
||||||
rebuildRemoteCompose,
|
|
||||||
updateApplicationStatus,
|
|
||||||
updateCompose,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import type { DeployJob } from "./schema";
|
|
||||||
import type { LemonSqueezyLicenseResponse } from "./types";
|
import type { LemonSqueezyLicenseResponse } from "./types";
|
||||||
|
|
||||||
// const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
export const validateLemonSqueezyLicense = async (
|
||||||
// const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
licenseKey: string,
|
||||||
// export const validateLemonSqueezyLicense = async (
|
): Promise<LemonSqueezyLicenseResponse> => {
|
||||||
// licenseKey: string,
|
|
||||||
// ): Promise<LemonSqueezyLicenseResponse> => {
|
|
||||||
// try {
|
|
||||||
// const response = await fetch(
|
|
||||||
// "https://api.lemonsqueezy.com/v1/licenses/validate",
|
|
||||||
// {
|
|
||||||
// method: "POST",
|
|
||||||
// headers: {
|
|
||||||
// "Content-Type": "application/json",
|
|
||||||
// "x-api-key": LEMON_SQUEEZY_API_KEY as string,
|
|
||||||
// },
|
|
||||||
// body: JSON.stringify({
|
|
||||||
// license_key: licenseKey,
|
|
||||||
// store_id: LEMON_SQUEEZY_STORE_ID as string,
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return response.json();
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Error validating license:", error);
|
|
||||||
// return { valid: false, error: "Error validating license" };
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const deploy = async (job: DeployJob) => {
|
|
||||||
try {
|
try {
|
||||||
if (job.applicationType === "application") {
|
const response = await fetch(
|
||||||
await updateApplicationStatus(job.applicationId, "running");
|
"https://api.lemonsqueezy.com/v1/licenses/validate",
|
||||||
if (job.server) {
|
{
|
||||||
if (job.type === "redeploy") {
|
method: "POST",
|
||||||
await rebuildRemoteApplication({
|
headers: {
|
||||||
applicationId: job.applicationId,
|
"Content-Type": "application/json",
|
||||||
titleLog: job.titleLog,
|
"x-api-key": LEMON_SQUEEZY_API_KEY as string,
|
||||||
descriptionLog: job.descriptionLog,
|
},
|
||||||
});
|
body: JSON.stringify({
|
||||||
} else if (job.type === "deploy") {
|
license_key: licenseKey,
|
||||||
await deployRemoteApplication({
|
store_id: LEMON_SQUEEZY_STORE_ID as string,
|
||||||
applicationId: job.applicationId,
|
}),
|
||||||
titleLog: job.titleLog,
|
},
|
||||||
descriptionLog: job.descriptionLog,
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (job.applicationType === "compose") {
|
|
||||||
await updateCompose(job.composeId, {
|
|
||||||
composeStatus: "running",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (job.server) {
|
return response.json();
|
||||||
if (job.type === "redeploy") {
|
|
||||||
await rebuildRemoteCompose({
|
|
||||||
composeId: job.composeId,
|
|
||||||
titleLog: job.titleLog,
|
|
||||||
descriptionLog: job.descriptionLog,
|
|
||||||
});
|
|
||||||
} else if (job.type === "deploy") {
|
|
||||||
await deployRemoteCompose({
|
|
||||||
composeId: job.composeId,
|
|
||||||
titleLog: job.titleLog,
|
|
||||||
descriptionLog: job.descriptionLog,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error("Error validating license:", error);
|
||||||
if (job.applicationType === "application") {
|
return { valid: false, error: "Error validating license" };
|
||||||
await updateApplicationStatus(job.applicationId, "error");
|
|
||||||
} else if (job.applicationType === "compose") {
|
|
||||||
await updateCompose(job.composeId, {
|
|
||||||
composeStatus: "error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Bundler",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"outDir": "dist",
|
"types": ["node"],
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "hono/jsx"
|
"jsxImportSource": "hono/jsx"
|
||||||
},
|
}
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ The following templates are available:
|
|||||||
- **Wordpress**: Open Source Content Management System
|
- **Wordpress**: Open Source Content Management System
|
||||||
- **Open WebUI**: Free and Open Source ChatGPT Alternative
|
- **Open WebUI**: Free and Open Source ChatGPT Alternative
|
||||||
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
|
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
|
||||||
- **Roundcube**: Free and open source webmail software for the masses, written in PHP, uses SMTP[^1].
|
|
||||||
|
|
||||||
|
|
||||||
## Create your own template
|
## Create your own template
|
||||||
|
|
||||||
@@ -40,5 +41,3 @@ We accept contributions to upload new templates to the dokploy repository.
|
|||||||
Make sure to follow the guidelines for creating a template:
|
Make sure to follow the guidelines for creating a template:
|
||||||
|
|
||||||
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
|
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
|
||||||
|
|
||||||
[^1]: Please note that if you're self-hosting a mail server you need port 25 to be open for SMTP (Mail Transmission Protocol that allows you to send and receive) to work properly. Some VPS providers like [Hetzner](https://docs.hetzner.com/cloud/servers/faq/#why-can-i-not-send-any-mails-from-my-server) block this port by default for new clients.
|
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
|||||||
p: ({ children }) => (
|
p: ({ children }) => (
|
||||||
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
|
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
|
||||||
),
|
),
|
||||||
li: ({ children, id }) => (
|
li: ({ children }) => (
|
||||||
<li {...{ id }} className="text-[#3E4342] dark:text-muted-foreground">
|
<li className="text-[#3E4342] dark:text-muted-foreground">{children}</li>
|
||||||
{children}
|
|
||||||
</li>
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,15 @@
|
|||||||
"react-ga4": "^2.1.0"
|
"react-ga4": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "10.4.12",
|
"tsx": "4.15.7",
|
||||||
|
"@biomejs/biome": "1.8.1",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
|
"@types/node": "^20.14.2",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.4",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addSuffixToAllProperties } from "@dokploy/server";
|
import { addSuffixToAllProperties } from "@/server/utils/docker/compose";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToConfigsRoot } from "@dokploy/server";
|
import { addSuffixToConfigsRoot } from "@/server/utils/docker/compose/configs";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToConfigsInServices } from "@dokploy/server";
|
import { addSuffixToConfigsInServices } from "@/server/utils/docker/compose/configs";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
import {
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
addSuffixToAllConfigs,
|
||||||
|
addSuffixToConfigsRoot,
|
||||||
|
} from "@/server/utils/docker/compose/configs";
|
||||||
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Domain } from "@dokploy/server";
|
import type { Domain } from "@/server/api/services/domain";
|
||||||
import { createDomainLabels } from "@dokploy/server";
|
import { createDomainLabels } from "@/server/utils/docker/domain";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("createDomainLabels", () => {
|
describe("createDomainLabels", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addDokployNetworkToRoot } from "@dokploy/server";
|
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("addDokployNetworkToRoot", () => {
|
describe("addDokployNetworkToRoot", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addDokployNetworkToService } from "@dokploy/server";
|
import { addDokployNetworkToService } from "@/server/utils/docker/domain";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("addDokployNetworkToService", () => {
|
describe("addDokployNetworkToService", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToServiceNetworks } from "@dokploy/server";
|
import { addSuffixToServiceNetworks } from "@/server/utils/docker/compose/network";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import {
|
import {
|
||||||
addSuffixToAllNetworks,
|
addSuffixToAllNetworks,
|
||||||
addSuffixToServiceNetworks,
|
addSuffixToServiceNetworks,
|
||||||
} from "@dokploy/server";
|
} from "@/server/utils/docker/compose/network";
|
||||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
import { addSuffixToSecretsRoot } from "@/server/utils/docker/compose/secrets";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToSecretsInServices } from "@dokploy/server";
|
import { addSuffixToSecretsInServices } from "@/server/utils/docker/compose/secrets";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addSuffixToAllSecrets } from "@dokploy/server";
|
import { addSuffixToAllSecrets } from "@/server/utils/docker/compose/secrets";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
addSuffixToAllServiceNames,
|
addSuffixToAllServiceNames,
|
||||||
addSuffixToServiceNames,
|
addSuffixToServiceNames,
|
||||||
} from "@dokploy/server";
|
} from "@/server/utils/docker/compose/service";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
|
import {
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
addSuffixToAllVolumes,
|
||||||
|
addSuffixToVolumesRoot,
|
||||||
|
} from "@/server/utils/docker/compose/volume";
|
||||||
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToVolumesRoot } from "@dokploy/server";
|
import { addSuffixToVolumesRoot } from "@/server/utils/docker/compose/volume";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import { addSuffixToVolumesInServices } from "@dokploy/server";
|
import { addSuffixToVolumesInServices } from "@/server/utils/docker/compose/volume";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||||
import {
|
import {
|
||||||
addSuffixToAllVolumes,
|
addSuffixToAllVolumes,
|
||||||
addSuffixToVolumesInServices,
|
addSuffixToVolumesInServices,
|
||||||
} from "@dokploy/server";
|
} from "@/server/utils/docker/compose/volume";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { paths } from "@dokploy/server/dist/constants";
|
import { paths } from "@/server/constants";
|
||||||
const { APPLICATIONS_PATH } = paths();
|
const { APPLICATIONS_PATH } = paths();
|
||||||
import type { ApplicationNested } from "@dokploy/server";
|
import type { ApplicationNested } from "@/server/utils/builders";
|
||||||
import { unzipDrop } from "@dokploy/server";
|
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
@@ -81,17 +81,14 @@ const baseApp: ApplicationNested = {
|
|||||||
username: null,
|
username: null,
|
||||||
dockerContextPath: null,
|
dockerContextPath: null,
|
||||||
};
|
};
|
||||||
|
//
|
||||||
|
vi.mock("@/server/constants", () => ({
|
||||||
|
paths: () => ({
|
||||||
|
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
|
}),
|
||||||
|
// APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
|
|
||||||
const actual = await importOriginal();
|
|
||||||
return {
|
|
||||||
// @ts-ignore
|
|
||||||
...actual,
|
|
||||||
paths: () => ({
|
|
||||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
describe("unzipDrop using real zip files", () => {
|
describe("unzipDrop using real zip files", () => {
|
||||||
// const { APPLICATIONS_PATH } = paths();
|
// const { APPLICATIONS_PATH } = paths();
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -105,19 +102,15 @@ describe("unzipDrop using real zip files", () => {
|
|||||||
it("should correctly extract a zip with a single root folder", async () => {
|
it("should correctly extract a zip with a single root folder", async () => {
|
||||||
baseApp.appName = "single-file";
|
baseApp.appName = "single-file";
|
||||||
// const appName = "single-file";
|
// const appName = "single-file";
|
||||||
try {
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
|
||||||
console.log(`Output Path: ${outputPath}`);
|
const zipBuffer = zip.toBuffer();
|
||||||
const zipBuffer = zip.toBuffer();
|
const file = new File([zipBuffer], "single.zip");
|
||||||
const file = new File([zipBuffer], "single.zip");
|
await unzipDrop(file, baseApp);
|
||||||
await unzipDrop(file, baseApp);
|
|
||||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
} finally {
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseRawConfig, processLogs } from "@dokploy/server";
|
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { Admin, FileConfig } from "@dokploy/server";
|
import type { Admin } from "@/server/api/services/admin";
|
||||||
import {
|
import { createDefaultServerTraefikConfig } from "@/server/setup/traefik-setup";
|
||||||
createDefaultServerTraefikConfig,
|
import { loadOrCreateConfig } from "@/server/utils/traefik/application";
|
||||||
loadOrCreateConfig,
|
import type { FileConfig } from "@/server/utils/traefik/file-types";
|
||||||
updateServerTraefik,
|
import { updateServerTraefik } from "@/server/utils/traefik/web-server";
|
||||||
} from "@dokploy/server";
|
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: Admin = {
|
const baseAdmin: Admin = {
|
||||||
@@ -24,9 +23,6 @@ const baseAdmin: Admin = {
|
|||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
enableLogRotation: false,
|
enableLogRotation: false,
|
||||||
serversQuantity: 0,
|
|
||||||
stripeCustomerId: "",
|
|
||||||
stripeSubscriptionId: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Domain } from "@dokploy/server";
|
import type { Domain } from "@/server/api/services/domain";
|
||||||
import type { Redirect } from "@dokploy/server";
|
import type { Redirect } from "@/server/api/services/redirect";
|
||||||
import type { ApplicationNested } from "@dokploy/server";
|
import type { ApplicationNested } from "@/server/utils/builders";
|
||||||
import { createRouterConfig } from "@dokploy/server";
|
import { createRouterConfig } from "@/server/utils/traefik/domain";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
|
|||||||
@@ -13,9 +13,4 @@ export default defineConfig({
|
|||||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||||
pool: "forks",
|
pool: "forks",
|
||||||
},
|
},
|
||||||
define: {
|
|
||||||
"process.env": {
|
|
||||||
NODE: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input, NumberInput } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -125,14 +125,28 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Published Port</FormLabel>
|
<FormLabel>Published Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<NumberInput placeholder="1-65535" {...field} />
|
<Input
|
||||||
|
placeholder="1-65535"
|
||||||
|
{...field}
|
||||||
|
value={field.value?.toString() || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
field.onChange(0);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
field.onChange(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="targetPort"
|
name="targetPort"
|
||||||
@@ -140,7 +154,22 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Target Port</FormLabel>
|
<FormLabel>Target Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="1-65535" {...field} />
|
<Input
|
||||||
|
placeholder="1-65535"
|
||||||
|
{...field}
|
||||||
|
value={field.value?.toString() || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
field.onChange(0);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
field.onChange(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific.
|
If you want to decrease or increase the resources to a specific
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -16,37 +16,20 @@ interface Props {
|
|||||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
|
|
||||||
setData("");
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
|
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
wsRef.current = ws; // Store WebSocket instance in ref
|
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
setData((currentData) => currentData + e.data);
|
setData((currentData) => currentData + e.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
return () => ws.close();
|
||||||
console.error("WebSocket error: ", error);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log("WebSocket connection closed");
|
|
||||||
wsRef.current = null; // Clear reference on close
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
||||||
ws.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
@@ -62,15 +45,7 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(e) => {
|
onOpenChange={(e) => {
|
||||||
onClose();
|
onClose();
|
||||||
if (!e) {
|
if (!e) setData("");
|
||||||
setData("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wsRef.current) {
|
|
||||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input, NumberInput } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -228,7 +228,13 @@ export const AddDomain = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Container Port</FormLabel>
|
<FormLabel>Container Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<NumberInput placeholder={"3000"} {...field} />
|
<Input
|
||||||
|
placeholder={"3000"}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(Number.parseInt(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To access the application it is required to set at least 1
|
To access to the application is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
|
|||||||
@@ -21,38 +21,20 @@ export const ShowDeploymentCompose = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
|
|
||||||
setData("");
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
wsRef.current = ws; // Store WebSocket instance in ref
|
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
setData((currentData) => currentData + e.data);
|
setData((currentData) => currentData + e.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
return () => ws.close();
|
||||||
console.error("WebSocket error: ", error);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log("WebSocket connection closed");
|
|
||||||
wsRef.current = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
||||||
ws.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
@@ -68,15 +50,7 @@ export const ShowDeploymentCompose = ({
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(e) => {
|
onOpenChange={(e) => {
|
||||||
onClose();
|
onClose();
|
||||||
if (!e) {
|
if (!e) setData("");
|
||||||
setData("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wsRef.current) {
|
|
||||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input, NumberInput } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -364,7 +364,13 @@ export const AddDomainCompose = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Container Port</FormLabel>
|
<FormLabel>Container Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<NumberInput placeholder={"3000"} {...field} />
|
<Input
|
||||||
|
placeholder={"3000"}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(Number.parseInt(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
|||||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To access to the application it is required to set at least 1
|
To access to the application is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
toast.error("Error to update the compose config");
|
toast.error("Error to update the compose config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Puzzle, RefreshCw } from "lucide-react";
|
import { Puzzle, RefreshCw } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -34,16 +34,6 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
|
|
||||||
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
|
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
mutateAsync({ composeId })
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch((err) => {});
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { FitAddon } from "xterm-addon-fit";
|
import { FitAddon } from "xterm-addon-fit";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
@@ -18,24 +18,12 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [term, setTerm] = React.useState<Terminal>();
|
const [term, setTerm] = React.useState<Terminal>();
|
||||||
const [lines, setLines] = React.useState<number>(40);
|
const [lines, setLines] = React.useState<number>(40);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
|
||||||
|
|
||||||
useEffect(() => {
|
const createTerminal = (): Terminal => {
|
||||||
// if (containerId === "select-a-container") {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
const container = document.getElementById(id);
|
const container = document.getElementById(id);
|
||||||
if (container) {
|
if (container) {
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wsRef.current) {
|
|
||||||
console.log(wsRef.current);
|
|
||||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.close();
|
|
||||||
}
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
const termi = new Terminal({
|
const termi = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cols: 80,
|
cols: 80,
|
||||||
@@ -57,7 +45,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
wsRef.current = ws;
|
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
termi.loadAddon(fitAddon);
|
termi.loadAddon(fitAddon);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -66,10 +54,6 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
termi.focus();
|
termi.focus();
|
||||||
setTerm(termi);
|
setTerm(termi);
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error("WebSocket error: ", error);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
termi.write(e.data);
|
termi.write(e.data);
|
||||||
};
|
};
|
||||||
@@ -78,14 +62,12 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
console.log(e.reason);
|
console.log(e.reason);
|
||||||
|
|
||||||
termi.write(`Connection closed!\nReason: ${e.reason}\n`);
|
termi.write(`Connection closed!\nReason: ${e.reason}\n`);
|
||||||
wsRef.current = null;
|
|
||||||
};
|
|
||||||
return () => {
|
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
||||||
ws.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
return termi;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
createTerminal();
|
||||||
}, [lines, containerId]);
|
}, [lines, containerId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific.
|
If you want to decrease or increase the resources to a specific
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl">Backups</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backups to your database to save the data to a different
|
Add backup to your database to save the data to a different
|
||||||
providers.
|
providers.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,8 +62,8 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
|||||||
<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">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup it is required to set at least 1 provider.
|
To create a backup is required to set at least 1 provider. Please,
|
||||||
Please, go to{" "}
|
go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -80,7 +79,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -91,7 +90,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
getIp,
|
ip,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific.
|
If you want to decrease or increase the resources to a specific
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
|
|||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl">Backups</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backups to your database to save the data to a different
|
Add backup to your database to save the data to a different
|
||||||
provider.
|
providers.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
|
|||||||
<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">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup it is required to set at least 1 provider.
|
To create a backup is required to set at least 1 provider. Please,
|
||||||
Please, go to{" "}
|
go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -80,7 +80,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -90,7 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
getIp,
|
ip,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const ShowVolumes = ({ mongoId }: 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 mongo use the following config.
|
If you want to persist data in this mongo use the following config
|
||||||
to setup the volumes
|
to setup the volumes
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export const DockerMonitoring = ({
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Monitoring</CardTitle>
|
<CardTitle className="text-xl">Monitoring</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Watch the usage of your server in the current app.
|
Watch the usage of your server in the current app
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific.
|
If you want to decrease or increase the resources to a specific
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
|
|||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl">Backups</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backups to your database to save the data to a different
|
Add backup to your database to save the data to a different
|
||||||
provider.
|
providers.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
|
|||||||
<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">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup it is required to set at least 1 provider.
|
To create a backup is required to set at least 1 provider. Please,
|
||||||
Please, go to{" "}
|
go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
@@ -80,7 +80,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -91,7 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
form,
|
form,
|
||||||
getIp,
|
ip,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowPostgresResources = ({ postgresId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific.
|
If you want to decrease or increase the resources to a specific
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
|||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<CardTitle className="text-xl">Backups</CardTitle>
|
<CardTitle className="text-xl">Backups</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add backups to your database to save the data to a different
|
Add backup to your database to save the data to a different
|
||||||
provider.
|
providers.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -63,8 +63,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
|||||||
<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">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup it is required to set at least 1 provider.
|
To create a backup is required to set at least 1 provider. Please,
|
||||||
Please, go to{" "}
|
go to{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/settings/server"
|
href="/dashboard/settings/server"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.postgres.saveExternalPort.useMutation();
|
api.postgres.saveExternalPort.useMutation();
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
@@ -80,9 +79,10 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
|
const hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
@@ -92,7 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
getIp,
|
ip,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { slugify } from "@/lib/slug";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CircuitBoard, HelpCircle } from "lucide-react";
|
import { CircuitBoard, HelpCircle } from "lucide-react";
|
||||||
import { useEffect, useState } 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";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -71,7 +71,6 @@ interface Props {
|
|||||||
|
|
||||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
@@ -102,7 +101,6 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
setVisible(false);
|
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
@@ -113,7 +111,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={visible} onOpenChange={setVisible}>
|
<Dialog>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
|||||||
@@ -15,25 +15,20 @@ import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
BookIcon,
|
BookIcon,
|
||||||
CircuitBoard,
|
|
||||||
ExternalLink,
|
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FolderInput,
|
FolderInput,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { UpdateProject } from "./update";
|
import { UpdateProject } from "./update";
|
||||||
|
|
||||||
@@ -50,7 +45,6 @@ export const ShowProjects = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data?.length === 0 && (
|
{data?.length === 0 && (
|
||||||
@@ -80,87 +74,17 @@ export const ShowProjects = () => {
|
|||||||
project?.redis.length +
|
project?.redis.length +
|
||||||
project?.applications.length +
|
project?.applications.length +
|
||||||
project?.compose.length;
|
project?.compose.length;
|
||||||
|
|
||||||
const flattedDomains = [
|
|
||||||
...project.applications.flatMap((a) => a.domains),
|
|
||||||
...project.compose.flatMap((a) => a.domains),
|
|
||||||
];
|
|
||||||
|
|
||||||
const renderDomainsDropdown = (
|
|
||||||
item: typeof project.compose | typeof project.applications,
|
|
||||||
) =>
|
|
||||||
item[0] ? (
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuLabel>
|
|
||||||
{"applicationId" in item[0] ? "Applications" : "Compose"}
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
{item.map((a) => (
|
|
||||||
<Fragment
|
|
||||||
key={"applicationId" in a ? a.applicationId : a.composeId}
|
|
||||||
>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs ">
|
|
||||||
{a.name}
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{a.domains.map((domain) => (
|
|
||||||
<DropdownMenuItem key={domain.domainId} asChild>
|
|
||||||
<Link
|
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
|
||||||
target="_blank"
|
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
|
||||||
>
|
|
||||||
<span>{domain.host}</span>
|
|
||||||
<ExternalLink className="size-4 shrink-0" />
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={project.projectId} className="w-full lg:max-w-md">
|
<div key={project.projectId} className="w-full lg:max-w-md">
|
||||||
<Link href={`/dashboard/project/${project.projectId}`}>
|
<Link href={`/dashboard/project/${project.projectId}`}>
|
||||||
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
||||||
{flattedDomains.length > 1 ? (
|
<Button
|
||||||
<DropdownMenu>
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
<DropdownMenuTrigger asChild>
|
size="sm"
|
||||||
<Button
|
variant="default"
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
>
|
||||||
size="sm"
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
variant="default"
|
</Button>
|
||||||
>
|
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-[200px] space-y-2"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{renderDomainsDropdown(project.applications)}
|
|
||||||
{renderDomainsDropdown(project.compose)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
) : flattedDomains[0] ? (
|
|
||||||
<Button
|
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
|
||||||
size="sm"
|
|
||||||
variant="default"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={`${flattedDomains[0].https ? "https" : "http"}://${flattedDomains[0].host}${flattedDomains[0].path}`}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center justify-between gap-2">
|
<CardTitle className="flex items-center justify-between gap-2">
|
||||||
<span className="flex flex-col gap-1.5">
|
<span className="flex flex-col gap-1.5">
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ShowRedisResources = ({ redisId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Resources</CardTitle>
|
<CardTitle className="text-xl">Resources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to decrease or increase the resources to a specific.
|
If you want to decrease or increase the resources to a specific
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
@@ -82,11 +81,11 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
return `redis://default:${data?.databasePassword}@${ip}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
}, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { NumberInput } from "@/components/ui/input";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { loadStripe } from "@stripe/stripe-js";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { AlertTriangle, CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
const stripePromise = loadStripe(
|
|
||||||
"pk_test_51QAm7bF3cxQuHeOz0xg04o9teeyTbbNHQPJ5Tr98MlTEan9MzewT3gwh0jSWBNvrRWZ5vASoBgxUSF4gPWsJwATk00Ir2JZ0S1",
|
|
||||||
);
|
|
||||||
|
|
||||||
export const calculatePrice = (count: number, isAnnual = false) => {
|
|
||||||
if (isAnnual) {
|
|
||||||
if (count <= 1) return 45.9;
|
|
||||||
return 35.7 * count;
|
|
||||||
}
|
|
||||||
if (count <= 1) return 4.5;
|
|
||||||
return count * 3.5;
|
|
||||||
};
|
|
||||||
// 178.156.147.118
|
|
||||||
export const ShowBilling = () => {
|
|
||||||
const { data: servers } = api.server.all.useQuery(undefined);
|
|
||||||
const { data: admin } = api.admin.one.useQuery();
|
|
||||||
const { data } = api.stripe.getProducts.useQuery();
|
|
||||||
const { mutateAsync: createCheckoutSession } =
|
|
||||||
api.stripe.createCheckoutSession.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: createCustomerPortalSession } =
|
|
||||||
api.stripe.createCustomerPortalSession.useMutation();
|
|
||||||
|
|
||||||
const [serverQuantity, setServerQuantity] = useState(3);
|
|
||||||
const [isAnnual, setIsAnnual] = useState(false);
|
|
||||||
|
|
||||||
const handleCheckout = async (productId: string) => {
|
|
||||||
const stripe = await stripePromise;
|
|
||||||
if (data && data.subscriptions.length === 0) {
|
|
||||||
createCheckoutSession({
|
|
||||||
productId,
|
|
||||||
serverQuantity: serverQuantity,
|
|
||||||
isAnnual,
|
|
||||||
}).then(async (session) => {
|
|
||||||
await stripe?.redirectToCheckout({
|
|
||||||
sessionId: session.sessionId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const products = data?.products.filter((product) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const interval = product?.default_price?.recurring?.interval;
|
|
||||||
return isAnnual ? interval === "year" : interval === "month";
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxServers = admin?.serversQuantity ?? 1;
|
|
||||||
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
|
||||||
const safePercentage = Math.min(percentage, 100);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4 w-full">
|
|
||||||
<Tabs
|
|
||||||
defaultValue="monthly"
|
|
||||||
value={isAnnual ? "annual" : "monthly"}
|
|
||||||
className="w-full"
|
|
||||||
onValueChange={(e) => setIsAnnual(e === "annual")}
|
|
||||||
>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
|
||||||
<TabsTrigger value="annual">Annual</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
{admin?.stripeSubscriptionId && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-lg font-medium">Servers Plan</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
You have {servers?.length} server on your plan of{" "}
|
|
||||||
{admin?.serversQuantity} servers
|
|
||||||
</p>
|
|
||||||
<div className="pb-5">
|
|
||||||
<Progress value={safePercentage} className="max-w-lg" />
|
|
||||||
</div>
|
|
||||||
{admin && (
|
|
||||||
<>
|
|
||||||
{admin.serversQuantity! <= servers?.length! && (
|
|
||||||
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
|
||||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
|
||||||
You have reached the maximum number of servers you can
|
|
||||||
create, please upgrade your plan to add more servers.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{products?.map((product) => {
|
|
||||||
const featured = true;
|
|
||||||
return (
|
|
||||||
<div key={product.id}>
|
|
||||||
<section
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
|
||||||
featured
|
|
||||||
? "order-first bg-black border py-8 lg:order-none"
|
|
||||||
: "lg:py-8",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isAnnual ? (
|
|
||||||
<div className="flex flex-row gap-2 items-center">
|
|
||||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
|
||||||
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
|
||||||
</p>
|
|
||||||
|
|
|
||||||
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
|
||||||
${" "}
|
|
||||||
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(2)}{" "}
|
|
||||||
/ Month USD
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
|
||||||
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<h3 className="mt-5 font-medium text-lg text-white">
|
|
||||||
{product.name}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className={clsx(
|
|
||||||
"text-sm",
|
|
||||||
featured ? "text-white" : "text-slate-400",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{product.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
role="list"
|
|
||||||
className={clsx(
|
|
||||||
" mt-4 flex flex-col gap-y-2 text-sm",
|
|
||||||
featured ? "text-white" : "text-slate-200",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{[
|
|
||||||
"All the features of Dokploy",
|
|
||||||
"Unlimited deployments",
|
|
||||||
"Self-hosted on your own infrastructure",
|
|
||||||
"Full access to all deployment features",
|
|
||||||
"Dokploy integration",
|
|
||||||
"Backups",
|
|
||||||
"All Incoming features",
|
|
||||||
].map((feature) => (
|
|
||||||
<li key={feature} className="flex text-muted-foreground">
|
|
||||||
<CheckIcon />
|
|
||||||
<span className="ml-4">{feature}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div className="flex flex-col gap-2 mt-4">
|
|
||||||
<div className="flex items-center gap-2 justify-center">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{serverQuantity} Servers
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
disabled={serverQuantity <= 1}
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (serverQuantity <= 1) return;
|
|
||||||
|
|
||||||
setServerQuantity(serverQuantity - 1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MinusIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<NumberInput
|
|
||||||
value={serverQuantity}
|
|
||||||
onChange={(e) => {
|
|
||||||
setServerQuantity(e.target.value as unknown as number);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setServerQuantity(serverQuantity + 1);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
data?.subscriptions && data?.subscriptions?.length > 0
|
|
||||||
? "justify-between"
|
|
||||||
: "justify-end",
|
|
||||||
"flex flex-row items-center gap-2 mt-4",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{admin?.stripeCustomerId && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
className="w-full"
|
|
||||||
onClick={async () => {
|
|
||||||
const session = await createCustomerPortalSession();
|
|
||||||
|
|
||||||
window.open(session.url);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Manage Subscription
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data?.subscriptions?.length === 0 && (
|
|
||||||
<div className="justify-end w-full">
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
onClick={async () => {
|
|
||||||
handleCheckout(product.id);
|
|
||||||
}}
|
|
||||||
disabled={serverQuantity < 1}
|
|
||||||
>
|
|
||||||
Subscribe
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -18,25 +18,10 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, HelpCircle } from "lucide-react";
|
import { AlertTriangle } 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";
|
||||||
@@ -50,7 +35,6 @@ const addCertificate = z.object({
|
|||||||
certificateData: z.string().min(1, "Certificate data is required"),
|
certificateData: z.string().min(1, "Certificate data is required"),
|
||||||
privateKey: z.string().min(1, "Private key is required"),
|
privateKey: z.string().min(1, "Private key is required"),
|
||||||
autoRenew: z.boolean().optional(),
|
autoRenew: z.boolean().optional(),
|
||||||
serverId: z.string().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddCertificate = z.infer<typeof addCertificate>;
|
type AddCertificate = z.infer<typeof addCertificate>;
|
||||||
@@ -60,7 +44,6 @@ export const AddCertificate = () => {
|
|||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.certificates.create.useMutation();
|
api.certificates.create.useMutation();
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
|
||||||
|
|
||||||
const form = useForm<AddCertificate>({
|
const form = useForm<AddCertificate>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -81,7 +64,6 @@ export const AddCertificate = () => {
|
|||||||
certificateData: data.certificateData,
|
certificateData: data.certificateData,
|
||||||
privateKey: data.privateKey,
|
privateKey: data.privateKey,
|
||||||
autoRenew: data.autoRenew,
|
autoRenew: data.autoRenew,
|
||||||
serverId: data.serverId,
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Certificate Created");
|
toast.success("Certificate Created");
|
||||||
@@ -162,47 +144,6 @@ export const AddCertificate = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="serverId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
|
||||||
Select a Server (Optional)
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
|
||||||
</FormLabel>
|
|
||||||
</TooltipTrigger>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a Server" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
{servers?.map((server) => (
|
|
||||||
<SelectItem
|
|
||||||
key={server.serverId}
|
|
||||||
value={server.serverId}
|
|
||||||
>
|
|
||||||
{server.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ export const ShowCertificates = () => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<ShieldCheck className="size-8 self-center text-muted-foreground" />
|
<ShieldCheck className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a certificate it is required to upload an existing
|
To create a certificate is required to upload your certificate
|
||||||
certificate
|
|
||||||
</span>
|
</span>
|
||||||
<AddCertificate />
|
<AddCertificate />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,18 +17,10 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Container } from "lucide-react";
|
import { AlertTriangle, Container } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
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";
|
||||||
@@ -44,9 +36,10 @@ const AddRegistrySchema = z.object({
|
|||||||
password: z.string().min(1, {
|
password: z.string().min(1, {
|
||||||
message: "Password is required",
|
message: "Password is required",
|
||||||
}),
|
}),
|
||||||
registryUrl: z.string(),
|
registryUrl: z.string().min(1, {
|
||||||
|
message: "Registry URL is required",
|
||||||
|
}),
|
||||||
imagePrefix: z.string(),
|
imagePrefix: z.string(),
|
||||||
serverId: z.string().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||||
@@ -55,9 +48,9 @@ export const AddRegistry = () => {
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
|
||||||
const { mutateAsync: testRegistry, isLoading } =
|
const { mutateAsync: testRegistry, isLoading } =
|
||||||
api.registry.testRegistry.useMutation();
|
api.registry.testRegistry.useMutation();
|
||||||
|
const router = useRouter();
|
||||||
const form = useForm<AddRegistry>({
|
const form = useForm<AddRegistry>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
@@ -65,7 +58,6 @@ export const AddRegistry = () => {
|
|||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
imagePrefix: "",
|
imagePrefix: "",
|
||||||
registryName: "",
|
registryName: "",
|
||||||
serverId: "",
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddRegistrySchema),
|
resolver: zodResolver(AddRegistrySchema),
|
||||||
});
|
});
|
||||||
@@ -75,7 +67,6 @@ export const AddRegistry = () => {
|
|||||||
const registryUrl = form.watch("registryUrl");
|
const registryUrl = form.watch("registryUrl");
|
||||||
const registryName = form.watch("registryName");
|
const registryName = form.watch("registryName");
|
||||||
const imagePrefix = form.watch("imagePrefix");
|
const imagePrefix = form.watch("imagePrefix");
|
||||||
const serverId = form.watch("serverId");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -83,7 +74,6 @@ export const AddRegistry = () => {
|
|||||||
password: "",
|
password: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
imagePrefix: "",
|
imagePrefix: "",
|
||||||
serverId: "",
|
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
@@ -95,7 +85,6 @@ export const AddRegistry = () => {
|
|||||||
registryUrl: data.registryUrl,
|
registryUrl: data.registryUrl,
|
||||||
registryType: "cloud",
|
registryType: "cloud",
|
||||||
imagePrefix: data.imagePrefix,
|
imagePrefix: data.imagePrefix,
|
||||||
serverId: data.serverId,
|
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
await utils.registry.all.invalidate();
|
await utils.registry.all.invalidate();
|
||||||
@@ -222,77 +211,34 @@ export const AddRegistry = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap">
|
||||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
<Button
|
||||||
<span className="text-sm text-muted-foreground">
|
type="button"
|
||||||
Select a server to test the registry. If you don't have a
|
variant={"secondary"}
|
||||||
server choose the default one.
|
isLoading={isLoading}
|
||||||
</span>
|
onClick={async () => {
|
||||||
<FormField
|
await testRegistry({
|
||||||
control={form.control}
|
username: username,
|
||||||
name="serverId"
|
password: password,
|
||||||
render={({ field }) => (
|
registryUrl: registryUrl,
|
||||||
<FormItem>
|
registryName: registryName,
|
||||||
<FormLabel>Server (Optional)</FormLabel>
|
registryType: "cloud",
|
||||||
<FormControl>
|
imagePrefix: imagePrefix,
|
||||||
<Select
|
})
|
||||||
onValueChange={field.onChange}
|
.then((data) => {
|
||||||
defaultValue={field.value}
|
if (data) {
|
||||||
>
|
toast.success("Registry Tested Successfully");
|
||||||
<SelectTrigger className="w-full">
|
} else {
|
||||||
<SelectValue placeholder="Select a server" />
|
toast.error("Registry Test Failed");
|
||||||
</SelectTrigger>
|
}
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Servers</SelectLabel>
|
|
||||||
{servers?.map((server) => (
|
|
||||||
<SelectItem
|
|
||||||
key={server.serverId}
|
|
||||||
value={server.serverId}
|
|
||||||
>
|
|
||||||
{server.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={"secondary"}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={async () => {
|
|
||||||
await testRegistry({
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
registryUrl: registryUrl,
|
|
||||||
registryName: registryName,
|
|
||||||
registryType: "cloud",
|
|
||||||
imagePrefix: imagePrefix,
|
|
||||||
serverId: serverId,
|
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.catch(() => {
|
||||||
if (data) {
|
toast.error("Error to test the registry");
|
||||||
toast.success("Registry Tested Successfully");
|
});
|
||||||
} else {
|
}}
|
||||||
toast.error("Registry Test Failed");
|
>
|
||||||
}
|
Test Registry
|
||||||
})
|
</Button>
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to test the registry");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Test Registry
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button isLoading={form.formState.isSubmitting} type="submit">
|
<Button isLoading={form.formState.isSubmitting} type="submit">
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AlertTriangle, Container } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const AddRegistrySchema = z.object({
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "Username is required",
|
||||||
|
})
|
||||||
|
.regex(/^[a-zA-Z0-9]+$/, {
|
||||||
|
message: "Username can only contain letters and numbers",
|
||||||
|
}),
|
||||||
|
password: z.string().min(1, {
|
||||||
|
message: "Password is required",
|
||||||
|
}),
|
||||||
|
registryUrl: z.string().min(1, {
|
||||||
|
message: "Registry URL is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||||
|
|
||||||
|
export const AddSelfHostedRegistry = () => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
|
api.registry.enableSelfHostedRegistry.useMutation();
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<AddRegistry>({
|
||||||
|
defaultValues: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
registryUrl: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(AddRegistrySchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
registryUrl: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: AddRegistry) => {
|
||||||
|
await mutateAsync({
|
||||||
|
registryUrl: data.registryUrl,
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
|
})
|
||||||
|
.then(async (data) => {
|
||||||
|
await utils.registry.all.invalidate();
|
||||||
|
toast.success("Self Hosted Registry Created");
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to create a self hosted registry");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="max-sm:w-full">
|
||||||
|
<Container className="h-4 w-4" />
|
||||||
|
Enable Self Hosted Registry
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:m:max-w-lg ">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add a self hosted registry</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Fill the next fields to add a self hosted registry.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg 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>
|
||||||
|
)}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Username" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Password"
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="registryUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Registry URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="registry.dokploy.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Point a DNS record to the VPS IP address.
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
import { AddRegistry } from "./add-docker-registry";
|
import { AddRegistry } from "./add-docker-registry";
|
||||||
|
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
|
||||||
import { DeleteRegistry } from "./delete-registry";
|
import { DeleteRegistry } from "./delete-registry";
|
||||||
import { UpdateDockerRegistry } from "./update-docker-registry";
|
import { UpdateDockerRegistry } from "./update-docker-registry";
|
||||||
|
|
||||||
@@ -30,6 +31,8 @@ export const ShowRegistry = () => {
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
|
||||||
|
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -40,10 +43,11 @@ export const ShowRegistry = () => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<Server className="size-8 self-center text-muted-foreground" />
|
<Server className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
To create a cluster it is required to set a registry.
|
To create a cluster is required to set a registry.
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||||
|
<AddSelfHostedRegistry />
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,15 +17,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
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";
|
||||||
@@ -43,9 +34,10 @@ const updateRegistry = z.object({
|
|||||||
message: "Username is required",
|
message: "Username is required",
|
||||||
}),
|
}),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
registryUrl: z.string(),
|
registryUrl: z.string().min(1, {
|
||||||
|
message: "Registry URL is required",
|
||||||
|
}),
|
||||||
imagePrefix: z.string(),
|
imagePrefix: z.string(),
|
||||||
serverId: z.string().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
||||||
@@ -56,8 +48,6 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
|
||||||
|
|
||||||
const { mutateAsync: testRegistry, isLoading } =
|
const { mutateAsync: testRegistry, isLoading } =
|
||||||
api.registry.testRegistry.useMutation();
|
api.registry.testRegistry.useMutation();
|
||||||
const { data, refetch } = api.registry.one.useQuery(
|
const { data, refetch } = api.registry.one.useQuery(
|
||||||
@@ -79,19 +69,15 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
serverId: "",
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(updateRegistry),
|
resolver: zodResolver(updateRegistry),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(form.formState.errors);
|
|
||||||
|
|
||||||
const password = form.watch("password");
|
const password = form.watch("password");
|
||||||
const username = form.watch("username");
|
const username = form.watch("username");
|
||||||
const registryUrl = form.watch("registryUrl");
|
const registryUrl = form.watch("registryUrl");
|
||||||
const registryName = form.watch("registryName");
|
const registryName = form.watch("registryName");
|
||||||
const imagePrefix = form.watch("imagePrefix");
|
const imagePrefix = form.watch("imagePrefix");
|
||||||
const serverId = form.watch("serverId");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -101,7 +87,6 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: data.username || "",
|
username: data.username || "",
|
||||||
password: "",
|
password: "",
|
||||||
registryUrl: data.registryUrl || "",
|
registryUrl: data.registryUrl || "",
|
||||||
serverId: "",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -114,7 +99,6 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
username: data.username,
|
username: data.username,
|
||||||
registryUrl: data.registryUrl,
|
registryUrl: data.registryUrl,
|
||||||
imagePrefix: data.imagePrefix,
|
imagePrefix: data.imagePrefix,
|
||||||
serverId: data.serverId,
|
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
toast.success("Registry Updated");
|
toast.success("Registry Updated");
|
||||||
@@ -240,47 +224,13 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
<DialogFooter
|
||||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
className={cn(
|
||||||
<span className="text-sm text-muted-foreground">
|
isCloud ? "sm:justify-between " : "",
|
||||||
Select a server to test the registry. If you don't have a server
|
"flex flex-row w-full gap-4 flex-wrap",
|
||||||
choose the default one.
|
)}
|
||||||
</span>
|
>
|
||||||
<FormField
|
{isCloud && (
|
||||||
control={form.control}
|
|
||||||
name="serverId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Server (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Select a server" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Servers</SelectLabel>
|
|
||||||
{servers?.map((server) => (
|
|
||||||
<SelectItem
|
|
||||||
key={server.serverId}
|
|
||||||
value={server.serverId}
|
|
||||||
>
|
|
||||||
{server.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
@@ -293,7 +243,6 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
registryName: registryName,
|
registryName: registryName,
|
||||||
registryType: "cloud",
|
registryType: "cloud",
|
||||||
imagePrefix: imagePrefix,
|
imagePrefix: imagePrefix,
|
||||||
serverId: serverId,
|
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -309,12 +258,12 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
|||||||
>
|
>
|
||||||
Test Registry
|
Test Registry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
isLoading={form.formState.isSubmitting}
|
||||||
type="submit"
|
|
||||||
form="hook-form"
|
form="hook-form"
|
||||||
|
type="submit"
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,16 +18,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
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 { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -42,15 +32,12 @@ const addDestination = z.object({
|
|||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
serverId: z.string().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddDestination = z.infer<typeof addDestination>;
|
type AddDestination = z.infer<typeof addDestination>;
|
||||||
|
|
||||||
export const AddDestination = () => {
|
export const AddDestination = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.destination.create.useMutation();
|
api.destination.create.useMutation();
|
||||||
@@ -202,106 +189,30 @@ export const AddDestination = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter
|
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||||
className={cn(
|
<Button
|
||||||
isCloud ? "!flex-col" : "flex-row",
|
isLoading={isLoadingConnection}
|
||||||
"flex w-full !justify-between pt-3 gap-4",
|
type="button"
|
||||||
)}
|
variant="secondary"
|
||||||
>
|
onClick={async () => {
|
||||||
{isCloud ? (
|
await testConnection({
|
||||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
accessKey: form.getValues("accessKeyId"),
|
||||||
<span className="text-sm text-muted-foreground">
|
bucket: form.getValues("bucket"),
|
||||||
Select a server to test the destination. If you don't have a
|
endpoint: form.getValues("endpoint"),
|
||||||
server choose the default one.
|
name: "Test",
|
||||||
</span>
|
region: form.getValues("region"),
|
||||||
<FormField
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
control={form.control}
|
})
|
||||||
name="serverId"
|
.then(async () => {
|
||||||
render={({ field }) => (
|
toast.success("Connection Success");
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Server (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Select a server" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Servers</SelectLabel>
|
|
||||||
{servers?.map((server) => (
|
|
||||||
<SelectItem
|
|
||||||
key={server.serverId}
|
|
||||||
value={server.serverId}
|
|
||||||
>
|
|
||||||
{server.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={"secondary"}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={async () => {
|
|
||||||
await testConnection({
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
serverId: form.getValues("serverId"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error("Error to connect the provider", {
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Test Connection
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
isLoading={isLoadingConnection}
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={async () => {
|
|
||||||
await testConnection({
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.catch(() => {
|
||||||
toast.success("Connection Success");
|
toast.error("Error to connect the provider");
|
||||||
})
|
});
|
||||||
.catch(() => {
|
}}
|
||||||
toast.error("Error to connect the provider");
|
>
|
||||||
});
|
Test connection
|
||||||
}}
|
</Button>
|
||||||
>
|
|
||||||
Test connection
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-destination-add"
|
form="hook-form-destination-add"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const ShowDestinations = () => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<FolderUp className="size-8 self-center text-muted-foreground" />
|
<FolderUp className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To create a backup it is required to set at least 1 provider.
|
To create a backup is required to set at least 1 provider.
|
||||||
</span>
|
</span>
|
||||||
<AddDestination />
|
<AddDestination />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,16 +18,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
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 { PenBoxIcon } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
@@ -43,7 +33,6 @@ const updateDestination = z.object({
|
|||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
serverId: z.string().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdateDestination = z.infer<typeof updateDestination>;
|
type UpdateDestination = z.infer<typeof updateDestination>;
|
||||||
@@ -54,8 +43,6 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateDestination = ({ destinationId }: Props) => {
|
export const UpdateDestination = ({ destinationId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data, refetch } = api.destination.one.useQuery(
|
const { data, refetch } = api.destination.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -233,107 +220,34 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter
|
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||||
className={cn(
|
|
||||||
isCloud ? "!flex-col" : "flex-row",
|
|
||||||
"flex w-full !justify-between pt-3 gap-4",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCloud ? (
|
|
||||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Select a server to test the destination. If you don't have a
|
|
||||||
server choose the default one.
|
|
||||||
</span>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="serverId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Server (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Select a server" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Servers</SelectLabel>
|
|
||||||
{servers?.map((server) => (
|
|
||||||
<SelectItem
|
|
||||||
key={server.serverId}
|
|
||||||
value={server.serverId}
|
|
||||||
>
|
|
||||||
{server.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={"secondary"}
|
|
||||||
onClick={async () => {
|
|
||||||
await testConnection({
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
serverId: form.getValues("serverId"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to connect the provider");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Test Connection
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
isLoading={isLoadingConnection}
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={async () => {
|
|
||||||
await testConnection({
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to connect the provider");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Test connection
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
isLoading={isLoadingConnection}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to connect the provider");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test connection
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
form="hook-form"
|
form="hook-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const AddGithubProvider = () => {
|
export const AddGithubProvider = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const url = useUrl();
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.auth.get.useQuery();
|
||||||
const [manifest, setManifest] = useState("");
|
const [manifest, setManifest] = useState("");
|
||||||
const [isOrganization, setIsOrganization] = useState(false);
|
const [isOrganization, setIsOrganization] = useState(false);
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
|
|||||||
export const AddNotification = () => {
|
export const AddNotification = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||||
api.notification.testSlackConnection.useMutation();
|
api.notification.testSlackConnection.useMutation();
|
||||||
|
|
||||||
@@ -660,28 +660,26 @@ export const AddNotification = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isCloud && (
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="dokployRestart"
|
||||||
name="dokployRestart"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
<div className="space-y-0.5">
|
||||||
<div className="space-y-0.5">
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<FormDescription>
|
||||||
<FormDescription>
|
Trigger the action when a dokploy is restarted.
|
||||||
Trigger the action when a dokploy is restarted.
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const ShowNotifications = () => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<BellRing className="size-8 self-center text-muted-foreground" />
|
<BellRing className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To send notifications it is required to set at least 1 provider.
|
To send notifications is required to set at least 1 provider.
|
||||||
</span>
|
</span>
|
||||||
<AddNotification />
|
<AddNotification />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
const telegramMutation = api.notification.updateTelegram.useMutation();
|
const telegramMutation = api.notification.updateTelegram.useMutation();
|
||||||
const discordMutation = api.notification.updateDiscord.useMutation();
|
const discordMutation = api.notification.updateDiscord.useMutation();
|
||||||
const emailMutation = api.notification.updateEmail.useMutation();
|
const emailMutation = api.notification.updateEmail.useMutation();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const form = useForm<NotificationSchema>({
|
const form = useForm<NotificationSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: "slack",
|
type: "slack",
|
||||||
@@ -618,29 +618,27 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!isCloud && (
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
defaultValue={form.control._defaultValues.dokployRestart}
|
||||||
defaultValue={form.control._defaultValues.dokployRestart}
|
name="dokployRestart"
|
||||||
name="dokployRestart"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
<div className="space-y-0.5">
|
||||||
<div className="space-y-0.5">
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<FormDescription>
|
||||||
<FormDescription>
|
Trigger the action when a dokploy is restarted.
|
||||||
Trigger the action when a dokploy is restarted.
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const ProfileForm = () => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Account</CardTitle>
|
<CardTitle className="text-xl">Account</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Change the details of your profile here.
|
Change your details of your profile here.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||||
@@ -145,6 +145,7 @@ export const ProfileForm = () => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
|
console.log(e);
|
||||||
field.onChange(e);
|
field.onChange(e);
|
||||||
}}
|
}}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ 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 { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -58,9 +57,6 @@ type Schema = z.infer<typeof Schema>;
|
|||||||
export const AddServer = () => {
|
export const AddServer = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data: canCreateMoreServers, refetch } =
|
|
||||||
api.stripe.canCreateMoreServers.useQuery();
|
|
||||||
|
|
||||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
@@ -86,10 +82,6 @@ export const AddServer = () => {
|
|||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refetch();
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -124,14 +116,6 @@ export const AddServer = () => {
|
|||||||
Add a server to deploy your applications remotely.
|
Add a server to deploy your applications remotely.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{!canCreateMoreServers && (
|
|
||||||
<AlertBlock type="warning">
|
|
||||||
You cannot create more servers,{" "}
|
|
||||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
|
||||||
Please upgrade your plan
|
|
||||||
</Link>
|
|
||||||
</AlertBlock>
|
|
||||||
)}
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -270,7 +254,6 @@ export const AddServer = () => {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
isLoading={form.formState.isSubmitting}
|
||||||
disabled={!canCreateMoreServers}
|
|
||||||
form="hook-form-add-server"
|
form="hook-form-add-server"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
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 copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
|
|||||||
@@ -36,9 +36,6 @@ export const ShowServers = () => {
|
|||||||
const { data, refetch } = api.server.all.useQuery();
|
const { data, refetch } = api.server.all.useQuery();
|
||||||
const { mutateAsync } = api.server.remove.useMutation();
|
const { mutateAsync } = api.server.remove.useMutation();
|
||||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const { data: canCreateMoreServers } =
|
|
||||||
api.stripe.canCreateMoreServers.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
@@ -77,22 +74,8 @@ export const ShowServers = () => {
|
|||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<ServerIcon className="size-8" />
|
<ServerIcon className="size-8" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
{!canCreateMoreServers ? (
|
No Servers found. Add a server to deploy your applications
|
||||||
<div>
|
remotely.
|
||||||
You cannot create more servers,{" "}
|
|
||||||
<Link
|
|
||||||
href="/dashboard/settings/billing"
|
|
||||||
className="text-primary"
|
|
||||||
>
|
|
||||||
Please upgrade your plan
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
No Servers found. Add a server to deploy your applications
|
|
||||||
remotely.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -104,9 +87,6 @@ export const ShowServers = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Name</TableHead>
|
<TableHead className="w-[100px]">Name</TableHead>
|
||||||
{isCloud && (
|
|
||||||
<TableHead className="text-center">Status</TableHead>
|
|
||||||
)}
|
|
||||||
<TableHead className="text-center">IP Address</TableHead>
|
<TableHead className="text-center">IP Address</TableHead>
|
||||||
<TableHead className="text-center">Port</TableHead>
|
<TableHead className="text-center">Port</TableHead>
|
||||||
<TableHead className="text-center">Username</TableHead>
|
<TableHead className="text-center">Username</TableHead>
|
||||||
@@ -118,23 +98,9 @@ export const ShowServers = () => {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((server) => {
|
{data?.map((server) => {
|
||||||
const canDelete = server.totalSum === 0;
|
const canDelete = server.totalSum === 0;
|
||||||
const isActive = server.serverStatus === "active";
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={server.serverId}>
|
<TableRow key={server.serverId}>
|
||||||
<TableCell className="w-[100px]">{server.name}</TableCell>
|
<TableCell className="w-[100px]">{server.name}</TableCell>
|
||||||
{isCloud && (
|
|
||||||
<TableHead className="text-center">
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
server.serverStatus === "active"
|
|
||||||
? "default"
|
|
||||||
: "destructive"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{server.serverStatus}
|
|
||||||
</Badge>
|
|
||||||
</TableHead>
|
|
||||||
)}
|
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge>{server.ipAddress}</Badge>
|
<Badge>{server.ipAddress}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -165,25 +131,18 @@ export const ShowServers = () => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
{server.sshKeyId && (
|
||||||
{isActive && (
|
<TerminalModal serverId={server.serverId}>
|
||||||
<>
|
<span>Enter the terminal</span>
|
||||||
{server.sshKeyId && (
|
</TerminalModal>
|
||||||
<TerminalModal serverId={server.serverId}>
|
|
||||||
<span>Enter the terminal</span>
|
|
||||||
</TerminalModal>
|
|
||||||
)}
|
|
||||||
<SetupServer serverId={server.serverId} />
|
|
||||||
|
|
||||||
<UpdateServer serverId={server.serverId} />
|
|
||||||
{server.sshKeyId && (
|
|
||||||
<ShowServerActions
|
|
||||||
serverId={server.serverId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SetupServer serverId={server.serverId} />
|
||||||
|
|
||||||
|
<UpdateServer serverId={server.serverId} />
|
||||||
|
{server.sshKeyId && (
|
||||||
|
<ShowServerActions serverId={server.serverId} />
|
||||||
|
)}
|
||||||
<DialogAction
|
<DialogAction
|
||||||
disabled={!canDelete}
|
disabled={!canDelete}
|
||||||
title={
|
title={
|
||||||
@@ -228,21 +187,17 @@ export const ShowServers = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
|
||||||
{isActive && (
|
{server.sshKeyId && (
|
||||||
<>
|
<>
|
||||||
{server.sshKeyId && (
|
<DropdownMenuSeparator />
|
||||||
<>
|
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
|
||||||
|
|
||||||
<ShowTraefikFileSystemModal
|
<ShowTraefikFileSystemModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
<ShowDockerContainersModal
|
<ShowDockerContainersModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -29,22 +29,11 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const addServerDomain = z
|
const addServerDomain = z.object({
|
||||||
.object({
|
domain: z.string().min(1, { message: "URL is required" }),
|
||||||
domain: z.string().min(1, { message: "URL is required" }),
|
letsEncryptEmail: z.string().min(1, "Email is required").email(),
|
||||||
letsEncryptEmail: z.string(),
|
certificateType: z.enum(["letsencrypt", "none"]),
|
||||||
certificateType: z.enum(["letsencrypt", "none"]),
|
});
|
||||||
})
|
|
||||||
.superRefine((data, ctx) => {
|
|
||||||
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message:
|
|
||||||
"LetsEncrypt email is required when certificate type is letsencrypt",
|
|
||||||
path: ["letsEncryptEmail"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
type AddServerDomain = z.infer<typeof addServerDomain>;
|
type AddServerDomain = z.infer<typeof addServerDomain>;
|
||||||
|
|
||||||
@@ -91,7 +80,7 @@ export const WebDomain = () => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Server Domain</CardTitle>
|
<CardTitle className="text-xl">Server Domain</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add a domain to your server application.
|
Add your server domain to your application
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user