mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2644b638d1 | ||
|
|
8785282133 | ||
|
|
35c084af1d | ||
|
|
b63488baba | ||
|
|
7e5f21b28e | ||
|
|
8d41bafb93 | ||
|
|
6f5049efd5 | ||
|
|
6dd6b636e5 | ||
|
|
8488d530f3 | ||
|
|
6c61d5cdf5 | ||
|
|
8036455c2d | ||
|
|
bf44eeab3d | ||
|
|
0cd185696d | ||
|
|
339697437a | ||
|
|
e0b15fe971 | ||
|
|
afc5ea43da | ||
|
|
bb337e819e | ||
|
|
160dd10f77 | ||
|
|
49b096fef6 | ||
|
|
4828b840cb | ||
|
|
67af70448b | ||
|
|
946acf5245 | ||
|
|
390b3a835a | ||
|
|
2842bf9a91 | ||
|
|
8a0e10f6f4 | ||
|
|
67efa82b91 | ||
|
|
546d6b87ea | ||
|
|
d012d19253 | ||
|
|
e925ed9ea4 | ||
|
|
0c05809d7d | ||
|
|
29f1631950 | ||
|
|
9c0c58035a | ||
|
|
f4262569dd | ||
|
|
99cf6eae49 | ||
|
|
0488546706 | ||
|
|
dc32cd71e5 | ||
|
|
efdfd5d13c | ||
|
|
d31cab76f0 | ||
|
|
00ed202127 | ||
|
|
aaa4ca297d | ||
|
|
629871e683 | ||
|
|
a237c651c3 | ||
|
|
4f9b0d9d59 | ||
|
|
b701a0b504 | ||
|
|
39036202bb | ||
|
|
4225ad83e7 | ||
|
|
38c4e0ede1 | ||
|
|
25a64c703f | ||
|
|
ab5871add7 | ||
|
|
2b0e009f6a | ||
|
|
9b6ea99eea | ||
|
|
c4cf545d85 | ||
|
|
1edd717432 | ||
|
|
5b88af6158 | ||
|
|
e995d894d8 | ||
|
|
5f56512e56 | ||
|
|
24e4930fc1 | ||
|
|
541728805f | ||
|
|
9e4bac1386 | ||
|
|
ed8d32d050 | ||
|
|
7fb66bc58b | ||
|
|
58c06fba86 | ||
|
|
3cf27a068a | ||
|
|
7cfbea3f60 | ||
|
|
7907e33431 | ||
|
|
89f3078ce5 | ||
|
|
f3ce69b656 | ||
|
|
43555cdabe | ||
|
|
651bf3a303 | ||
|
|
4cde1a8a7d | ||
|
|
405efcac0b | ||
|
|
8397de0dca | ||
|
|
06b58e6495 | ||
|
|
24db4006cf | ||
|
|
84009c5e9b | ||
|
|
e32afde973 | ||
|
|
fecffac573 | ||
|
|
b4448e013c | ||
|
|
c3f06a6272 | ||
|
|
2be724f780 | ||
|
|
bf78326c96 | ||
|
|
4ca8722c6e | ||
|
|
e56e1eb687 | ||
|
|
5a5c302bdc | ||
|
|
997dc85985 | ||
|
|
09ef851372 | ||
|
|
7c4987d84d | ||
|
|
5cebf5540a | ||
|
|
3df2f8e58c | ||
|
|
a642d36a23 | ||
|
|
72bceec62d | ||
|
|
3d32314e80 | ||
|
|
7259830ac1 | ||
|
|
daa87c0dc7 | ||
|
|
a2ee55e0e9 | ||
|
|
de6aeac243 | ||
|
|
f640b4a87f | ||
|
|
3747db08d4 | ||
|
|
ab4677ac0e | ||
|
|
172d55311e | ||
|
|
3ce25e2ac8 | ||
|
|
388ded9aa5 | ||
|
|
767d3e1944 | ||
|
|
ec1d6c7430 | ||
|
|
8abeae5e63 | ||
|
|
cc90d9ec9b | ||
|
|
6b5de00fb0 | ||
|
|
5ed96fb0ce | ||
|
|
a73af1d578 | ||
|
|
5867a27901 | ||
|
|
8f7bffc349 | ||
|
|
21ee22d4f5 | ||
|
|
fb72132a4b | ||
|
|
ca904c15d9 | ||
|
|
682863f83e | ||
|
|
acd722678e | ||
|
|
3750977f41 | ||
|
|
9b401059b0 | ||
|
|
6a3ef5c860 | ||
|
|
a36518a8f0 | ||
|
|
a5eb4b0a72 | ||
|
|
b5c0876dd4 | ||
|
|
9745d12ac8 | ||
|
|
5c72e5a452 | ||
|
|
600f4b2106 | ||
|
|
c12d37fe0a | ||
|
|
bba8d00ba2 | ||
|
|
78665ffbfa | ||
|
|
d41c8c70c3 | ||
|
|
f13e5d449c | ||
|
|
d256998677 | ||
|
|
4aaf04ce74 | ||
|
|
ecfca9419a | ||
|
|
dfd6764320 | ||
|
|
73bf5274f5 | ||
|
|
fc38a42587 | ||
|
|
9b255964fe | ||
|
|
29f55ca1a0 | ||
|
|
6a5fb8faff | ||
|
|
5c225c8d42 | ||
|
|
c1c5fc978b | ||
|
|
ffd19f591d | ||
|
|
81a41a7f31 | ||
|
|
1c9b704ecc | ||
|
|
edf1fdedf0 | ||
|
|
8484649071 | ||
|
|
1e68248611 | ||
|
|
b3e35c5838 | ||
|
|
ddd4ba8135 | ||
|
|
539544d0de | ||
|
|
123b5d098b | ||
|
|
796a9ca11f | ||
|
|
2872ef3ccb | ||
|
|
06a772e344 | ||
|
|
e99666f4c0 | ||
|
|
bd243d79e2 | ||
|
|
18b4b23f79 | ||
|
|
071a9d5104 | ||
|
|
61ebd1b16e | ||
|
|
9836c988a0 | ||
|
|
03d7738032 | ||
|
|
98aa474975 | ||
|
|
7bd6b66551 | ||
|
|
727e50648e | ||
|
|
0b2b20caeb | ||
|
|
6cc64b4454 | ||
|
|
349bc89851 | ||
|
|
7046d05f63 | ||
|
|
cef21ac8b5 | ||
|
|
2ae7e562bb | ||
|
|
e4b998c608 | ||
|
|
9b7aacc934 | ||
|
|
7027f39c48 | ||
|
|
0aff344bc0 | ||
|
|
4715f34e15 | ||
|
|
59386ed4b7 | ||
|
|
5b0bf99cbf | ||
|
|
8e227a3286 | ||
|
|
869e58739f |
@@ -11,6 +11,7 @@ jobs:
|
||||
command: |
|
||||
cp apps/dokploy/.env.production.example .env.production
|
||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||
|
||||
- run:
|
||||
name: Build and push AMD64 image
|
||||
command: |
|
||||
@@ -61,7 +62,7 @@ jobs:
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
echo $VERSION
|
||||
TAG="latest"
|
||||
|
||||
|
||||
docker manifest create dokploy/dokploy:${TAG} \
|
||||
dokploy/dokploy:${TAG}-amd64 \
|
||||
dokploy/dokploy:${TAG}-arm64
|
||||
|
||||
BIN
.github/sponsors/lxaer.png
vendored
BIN
.github/sponsors/lxaer.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 248 KiB |
73
.github/workflows/deploy.yml
vendored
73
.github/workflows/deploy.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build Docs & Website Docker images
|
||||
name: Build Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -48,3 +48,74 @@ jobs:
|
||||
push: true
|
||||
tags: dokploy/website:latest
|
||||
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,6 +18,7 @@ jobs:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm biome ci
|
||||
- run: pnpm typecheck
|
||||
|
||||
@@ -32,6 +33,7 @@ jobs:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm build
|
||||
|
||||
parallel-tests:
|
||||
@@ -44,4 +46,5 @@ jobs:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm test
|
||||
|
||||
@@ -15,7 +15,9 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm 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
|
||||
|
||||
52
Dockerfile.cloud
Normal file
52
Dockerfile.cloud
Normal file
@@ -0,0 +1,52 @@
|
||||
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" ]
|
||||
36
Dockerfile.schedule
Normal file
36
Dockerfile.schedule
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
36
Dockerfile.server
Normal file
36
Dockerfile.server
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
11
README.md
11
README.md
@@ -32,6 +32,7 @@ Dokploy include multiples features to make your life easier.
|
||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||
- **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.)
|
||||
- **Multi Server**: Deploy and manager your applications remotely to external servers.
|
||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
## 🚀 Getting Started
|
||||
@@ -58,7 +59,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
### Hero Sponsors 🎖
|
||||
|
||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" ><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/></a>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||
<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 🥇
|
||||
|
||||
@@ -81,6 +89,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
||||
</div>
|
||||
|
||||
#### Organizations:
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"name": "@dokploy/api",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts"
|
||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"zod": "^3.23.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"hono": "^4.5.8",
|
||||
"dotenv": "^16.3.1"
|
||||
"dotenv": "^16.3.1",
|
||||
"redis": "4.7.0",
|
||||
"@nerimity/mimiqueue": "1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.2",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0"
|
||||
}
|
||||
|
||||
@@ -1,66 +1,61 @@
|
||||
import { serve } from "@hono/node-server";
|
||||
import { config } from "dotenv";
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { validateLemonSqueezyLicense } from "./utils";
|
||||
|
||||
config();
|
||||
import "dotenv/config";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { Queue } from "@nerimity/mimiqueue";
|
||||
import { createClient } from "redis";
|
||||
import { logger } from "./logger";
|
||||
import { type DeployJob, deployJobSchema } from "./schema";
|
||||
import { deploy } from "./utils";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
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!");
|
||||
const redisClient = createClient({
|
||||
url: process.env.REDIS_URL,
|
||||
});
|
||||
|
||||
app.post("/v1/validate-license", async (c) => {
|
||||
const { licenseKey } = await c.req.json();
|
||||
app.use(async (c, next) => {
|
||||
if (c.req.path === "/health") {
|
||||
return next();
|
||||
}
|
||||
const authHeader = c.req.header("X-API-Key");
|
||||
|
||||
if (!licenseKey) {
|
||||
return c.json({ error: "License key is required" }, 400);
|
||||
if (process.env.API_KEY !== authHeader) {
|
||||
return c.json({ message: "Invalid API Key" }, 403);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
const port = 4000;
|
||||
console.log(`Server is running on port ${port}`);
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||
const data = c.req.valid("json");
|
||||
const res = queue.add(data, { groupName: data.serverId });
|
||||
return c.json(
|
||||
{
|
||||
message: "Deployment Added",
|
||||
},
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/health", async (c) => {
|
||||
return c.json({ status: "ok" });
|
||||
});
|
||||
|
||||
const queue = new Queue({
|
||||
name: "deployments",
|
||||
process: async (job: DeployJob) => {
|
||||
logger.info("Deploying job", job);
|
||||
return await deploy(job);
|
||||
},
|
||||
redisClient,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await redisClient.connect();
|
||||
await redisClient.flushAll();
|
||||
logger.info("Redis Cleaned");
|
||||
})();
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || "3000");
|
||||
logger.info("Starting Deployments Server ✅", port);
|
||||
serve({ fetch: app.fetch, port });
|
||||
|
||||
10
apps/api/src/logger.ts
Normal file
10
apps/api/src/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import pino from "pino";
|
||||
|
||||
export const logger = pino({
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
24
apps/api/src/schema.ts
Normal file
24
apps/api/src/schema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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(),
|
||||
}),
|
||||
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(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||
@@ -1,28 +1,96 @@
|
||||
import { LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_STORE_ID } from ".";
|
||||
import {
|
||||
deployApplication,
|
||||
deployCompose,
|
||||
deployRemoteApplication,
|
||||
deployRemoteCompose,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
rebuildRemoteApplication,
|
||||
rebuildRemoteCompose,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
} from "@dokploy/server";
|
||||
import type { DeployJob } from "./schema";
|
||||
import type { LemonSqueezyLicenseResponse } from "./types";
|
||||
|
||||
export const validateLemonSqueezyLicense = async (
|
||||
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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
// const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
||||
// const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
||||
// export const validateLemonSqueezyLicense = async (
|
||||
// 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();
|
||||
// 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 {
|
||||
if (job.applicationType === "application") {
|
||||
await updateApplicationStatus(job.applicationId, "running");
|
||||
if (job.server) {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (job.applicationType === "compose") {
|
||||
await updateCompose(job.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
|
||||
if (job.server) {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error validating license:", error);
|
||||
return { valid: false, error: "Error validating license" };
|
||||
console.log(error);
|
||||
if (job.applicationType === "application") {
|
||||
await updateApplicationStatus(job.applicationId, "error");
|
||||
} else if (job.applicationType === "compose") {
|
||||
await updateCompose(job.composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"],
|
||||
"outDir": "dist",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ The following templates are available:
|
||||
- **Wordpress**: Open Source Content Management System
|
||||
- **Open WebUI**: Free and Open Source ChatGPT Alternative
|
||||
- **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
|
||||
|
||||
@@ -41,3 +40,5 @@ We accept contributions to upload new templates to the dokploy repository.
|
||||
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)
|
||||
|
||||
[^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,8 +10,10 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
p: ({ children }) => (
|
||||
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-[#3E4342] dark:text-muted-foreground">{children}</li>
|
||||
li: ({ children, id }) => (
|
||||
<li {...{ id }} className="text-[#3E4342] dark:text-muted-foreground">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,15 +21,11 @@
|
||||
"react-ga4": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "4.15.7",
|
||||
"@biomejs/biome": "1.8.1",
|
||||
"autoprefixer": "10.4.12",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { addSuffixToAllProperties } from "@/server/utils/docker/compose";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { addSuffixToAllProperties } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToConfigsRoot } from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToConfigsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToConfigsInServices } from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToConfigsInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addSuffixToAllConfigs,
|
||||
addSuffixToConfigsRoot,
|
||||
} from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Domain } from "@/server/api/services/domain";
|
||||
import { createDomainLabels } from "@/server/utils/docker/domain";
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import { createDomainLabels } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("createDomainLabels", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain";
|
||||
import { addDokployNetworkToRoot } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("addDokployNetworkToRoot", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addDokployNetworkToService } from "@/server/utils/docker/domain";
|
||||
import { addDokployNetworkToService } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("addDokployNetworkToService", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNetworks } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNetworks } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import {
|
||||
addSuffixToAllNetworks,
|
||||
addSuffixToServiceNetworks,
|
||||
} from "@/server/utils/docker/compose/network";
|
||||
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
} from "@dokploy/server";
|
||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToSecretsRoot } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToSecretsInServices } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToSecretsInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { addSuffixToAllSecrets } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { addSuffixToAllSecrets } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
addSuffixToAllServiceNames,
|
||||
addSuffixToServiceNames,
|
||||
} from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
} from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addSuffixToAllVolumes,
|
||||
addSuffixToVolumesRoot,
|
||||
} from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToVolumesRoot } from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToVolumesRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToVolumesInServices } from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToVolumesInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import {
|
||||
addSuffixToAllVolumes,
|
||||
addSuffixToVolumesInServices,
|
||||
} from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
} from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { paths } from "@/server/constants";
|
||||
import { paths } from "@dokploy/server/dist/constants";
|
||||
const { APPLICATIONS_PATH } = paths();
|
||||
import type { ApplicationNested } from "@/server/utils/builders";
|
||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { unzipDrop } from "@dokploy/server";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -81,14 +81,17 @@ const baseApp: ApplicationNested = {
|
||||
username: 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", () => {
|
||||
// const { APPLICATIONS_PATH } = paths();
|
||||
beforeAll(async () => {
|
||||
@@ -102,15 +105,19 @@ describe("unzipDrop using real zip files", () => {
|
||||
it("should correctly extract a zip with a single root folder", async () => {
|
||||
baseApp.appName = "single-file";
|
||||
// const appName = "single-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
try {
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||
console.log(`Output Path: ${outputPath}`);
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils";
|
||||
import { parseRawConfig, processLogs } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ vi.mock("node:fs", () => ({
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { Admin } from "@/server/api/services/admin";
|
||||
import { createDefaultServerTraefikConfig } from "@/server/setup/traefik-setup";
|
||||
import { loadOrCreateConfig } from "@/server/utils/traefik/application";
|
||||
import type { FileConfig } from "@/server/utils/traefik/file-types";
|
||||
import { updateServerTraefik } from "@/server/utils/traefik/web-server";
|
||||
import type { Admin, FileConfig } from "@dokploy/server";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
updateServerTraefik,
|
||||
} from "@dokploy/server";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: Admin = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Domain } from "@/server/api/services/domain";
|
||||
import type { Redirect } from "@/server/api/services/redirect";
|
||||
import type { ApplicationNested } from "@/server/utils/builders";
|
||||
import { createRouterConfig } from "@/server/utils/traefik/domain";
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import type { Redirect } from "@dokploy/server";
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { createRouterConfig } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
|
||||
@@ -13,4 +13,9 @@ export default defineConfig({
|
||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||
pool: "forks",
|
||||
},
|
||||
define: {
|
||||
"process.env": {
|
||||
NODE: "test",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -125,28 +125,14 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Published Port</FormLabel>
|
||||
<FormControl>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder="1-65535" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="targetPort"
|
||||
@@ -154,22 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Target Port</FormLabel>
|
||||
<FormControl>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input placeholder="1-65535" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<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
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -16,20 +16,37 @@ interface Props {
|
||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !logPath) return;
|
||||
|
||||
setData("");
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws; // Store WebSocket instance in ref
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
setData((currentData) => currentData + e.data);
|
||||
};
|
||||
|
||||
return () => ws.close();
|
||||
ws.onerror = (error) => {
|
||||
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]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
@@ -45,7 +62,15 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
open={open}
|
||||
onOpenChange={(e) => {
|
||||
onClose();
|
||||
if (!e) setData("");
|
||||
if (!e) {
|
||||
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"}>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -228,13 +228,7 @@ export const AddDomain = ({
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"3000"}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To access to the application is required to set at least 1
|
||||
To access the application it is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
|
||||
@@ -21,20 +21,38 @@ export const ShowDeploymentCompose = ({
|
||||
}: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !logPath) return;
|
||||
|
||||
setData("");
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
wsRef.current = ws; // Store WebSocket instance in ref
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
setData((currentData) => currentData + e.data);
|
||||
};
|
||||
|
||||
return () => ws.close();
|
||||
ws.onerror = (error) => {
|
||||
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]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
@@ -50,7 +68,15 @@ export const ShowDeploymentCompose = ({
|
||||
open={open}
|
||||
onOpenChange={(e) => {
|
||||
onClose();
|
||||
if (!e) setData("");
|
||||
if (!e) {
|
||||
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"}>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -364,13 +364,7 @@ export const AddDomainCompose = ({
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"3000"}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To access to the application is required to set at least 1
|
||||
To access to the application it is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
|
||||
@@ -77,7 +77,6 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
toast.error("Error to update the compose config");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { Puzzle, RefreshCw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
@@ -34,6 +34,16 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
|
||||
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
mutateAsync({ composeId })
|
||||
.then(() => {
|
||||
refetch();
|
||||
})
|
||||
.catch((err) => {});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
@@ -18,12 +18,24 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
}) => {
|
||||
const [term, setTerm] = React.useState<Terminal>();
|
||||
const [lines, setLines] = React.useState<number>(40);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
|
||||
const createTerminal = (): Terminal => {
|
||||
useEffect(() => {
|
||||
// if (containerId === "select-a-container") {
|
||||
// return;
|
||||
// }
|
||||
const container = document.getElementById(id);
|
||||
if (container) {
|
||||
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({
|
||||
cursorBlink: true,
|
||||
cols: 80,
|
||||
@@ -45,7 +57,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
wsRef.current = ws;
|
||||
const fitAddon = new FitAddon();
|
||||
termi.loadAddon(fitAddon);
|
||||
// @ts-ignore
|
||||
@@ -54,6 +66,10 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
termi.focus();
|
||||
setTerm(termi);
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error: ", error);
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
termi.write(e.data);
|
||||
};
|
||||
@@ -62,12 +78,14 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
console.log(e.reason);
|
||||
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<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
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<CardTitle className="text-xl">Backups</CardTitle>
|
||||
<CardDescription>
|
||||
Add backup to your database to save the data to a different
|
||||
Add backups to your database to save the data to a different
|
||||
providers.
|
||||
</CardDescription>
|
||||
</div>
|
||||
@@ -62,8 +62,8 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a backup is required to set at least 1 provider. Please,
|
||||
go to{" "}
|
||||
To create a backup it is required to set at least 1 provider.
|
||||
Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-foreground"
|
||||
|
||||
@@ -48,6 +48,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
@@ -79,7 +80,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
const buildConnectionUrl = () => {
|
||||
const port = form.watch("externalPort") || data?.externalPort;
|
||||
|
||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||
};
|
||||
|
||||
setConnectionUrl(buildConnectionUrl());
|
||||
@@ -90,7 +91,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
form,
|
||||
data?.databaseName,
|
||||
data?.databaseUser,
|
||||
ip,
|
||||
getIp,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<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
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,8 +44,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<CardTitle className="text-xl">Backups</CardTitle>
|
||||
<CardDescription>
|
||||
Add backup to your database to save the data to a different
|
||||
providers.
|
||||
Add backups to your database to save the data to a different
|
||||
provider.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
@@ -62,8 +62,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a backup is required to set at least 1 provider. Please,
|
||||
go to{" "}
|
||||
To create a backup it is required to set at least 1 provider.
|
||||
Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-foreground"
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
@@ -80,7 +80,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
const buildConnectionUrl = () => {
|
||||
const port = form.watch("externalPort") || data?.externalPort;
|
||||
|
||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}`;
|
||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
||||
};
|
||||
|
||||
setConnectionUrl(buildConnectionUrl());
|
||||
@@ -90,7 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
data?.databasePassword,
|
||||
form,
|
||||
data?.databaseUser,
|
||||
ip,
|
||||
getIp,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ShowVolumes = ({ mongoId }: Props) => {
|
||||
<div>
|
||||
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||
<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
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
@@ -191,7 +191,7 @@ export const DockerMonitoring = ({
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Monitoring</CardTitle>
|
||||
<CardDescription>
|
||||
Watch the usage of your server in the current app
|
||||
Watch the usage of your server in the current app.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<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
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,8 +44,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<CardTitle className="text-xl">Backups</CardTitle>
|
||||
<CardDescription>
|
||||
Add backup to your database to save the data to a different
|
||||
providers.
|
||||
Add backups to your database to save the data to a different
|
||||
provider.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
@@ -62,8 +62,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a backup is required to set at least 1 provider. Please,
|
||||
go to{" "}
|
||||
To create a backup it is required to set at least 1 provider.
|
||||
Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-foreground"
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
@@ -80,7 +80,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
const buildConnectionUrl = () => {
|
||||
const port = form.watch("externalPort") || data?.externalPort;
|
||||
|
||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||
};
|
||||
|
||||
setConnectionUrl(buildConnectionUrl());
|
||||
@@ -91,7 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
data?.databaseName,
|
||||
data?.databaseUser,
|
||||
form,
|
||||
ip,
|
||||
getIp,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ShowPostgresResources = ({ postgresId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<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
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -45,8 +45,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<CardTitle className="text-xl">Backups</CardTitle>
|
||||
<CardDescription>
|
||||
Add backup to your database to save the data to a different
|
||||
providers.
|
||||
Add backups to your database to save the data to a different
|
||||
provider.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
@@ -63,8 +63,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a backup is required to set at least 1 provider. Please,
|
||||
go to{" "}
|
||||
To create a backup it is required to set at least 1 provider.
|
||||
Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-foreground"
|
||||
|
||||
@@ -48,6 +48,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||
const { mutateAsync, isLoading } =
|
||||
api.postgres.saveExternalPort.useMutation();
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
@@ -79,10 +80,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
const buildConnectionUrl = () => {
|
||||
const hostname = window.location.hostname;
|
||||
const port = form.watch("externalPort") || data?.externalPort;
|
||||
|
||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||
};
|
||||
|
||||
setConnectionUrl(buildConnectionUrl());
|
||||
@@ -92,7 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
data?.databasePassword,
|
||||
form,
|
||||
data?.databaseName,
|
||||
ip,
|
||||
getIp,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -39,7 +39,7 @@ import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CircuitBoard, HelpCircle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -71,6 +71,7 @@ interface Props {
|
||||
|
||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
@@ -101,6 +102,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
setVisible(false);
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
@@ -111,7 +113,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={visible} onOpenChange={setVisible}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
|
||||
@@ -15,20 +15,25 @@ import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
AlertTriangle,
|
||||
BookIcon,
|
||||
CircuitBoard,
|
||||
ExternalLink,
|
||||
ExternalLinkIcon,
|
||||
FolderInput,
|
||||
MoreHorizontalIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { UpdateProject } from "./update";
|
||||
|
||||
@@ -45,6 +50,7 @@ export const ShowProjects = () => {
|
||||
},
|
||||
);
|
||||
const { mutateAsync } = api.project.remove.useMutation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.length === 0 && (
|
||||
@@ -74,17 +80,87 @@ export const ShowProjects = () => {
|
||||
project?.redis.length +
|
||||
project?.applications.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 (
|
||||
<div key={project.projectId} className="w-full lg:max-w-md">
|
||||
<Link href={`/dashboard/project/${project.projectId}`}>
|
||||
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
||||
<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"
|
||||
>
|
||||
<ExternalLinkIcon className="size-3.5" />
|
||||
</Button>
|
||||
{flattedDomains.length > 1 ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<CardTitle className="flex items-center justify-between gap-2">
|
||||
<span className="flex flex-col gap-1.5">
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ShowRedisResources = ({ redisId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<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
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -48,6 +48,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
|
||||
const form = useForm<DockerProvider>({
|
||||
defaultValues: {},
|
||||
@@ -81,11 +82,11 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||
const hostname = window.location.hostname;
|
||||
const port = form.watch("externalPort") || data?.externalPort;
|
||||
|
||||
return `redis://default:${data?.databasePassword}@${ip}:${port}`;
|
||||
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
||||
};
|
||||
|
||||
setConnectionUrl(buildConnectionUrl());
|
||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]);
|
||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
|
||||
@@ -18,10 +18,25 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
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 {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { AlertTriangle, HelpCircle } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -35,6 +50,7 @@ const addCertificate = z.object({
|
||||
certificateData: z.string().min(1, "Certificate data is required"),
|
||||
privateKey: z.string().min(1, "Private key is required"),
|
||||
autoRenew: z.boolean().optional(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddCertificate = z.infer<typeof addCertificate>;
|
||||
@@ -44,6 +60,7 @@ export const AddCertificate = () => {
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.certificates.create.useMutation();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
const form = useForm<AddCertificate>({
|
||||
defaultValues: {
|
||||
@@ -64,6 +81,7 @@ export const AddCertificate = () => {
|
||||
certificateData: data.certificateData,
|
||||
privateKey: data.privateKey,
|
||||
autoRenew: data.autoRenew,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Certificate Created");
|
||||
@@ -144,6 +162,47 @@ export const AddCertificate = () => {
|
||||
</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>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
|
||||
@@ -27,7 +27,8 @@ export const ShowCertificates = () => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<ShieldCheck className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a certificate is required to upload your certificate
|
||||
To create a certificate it is required to upload an existing
|
||||
certificate
|
||||
</span>
|
||||
<AddCertificate />
|
||||
</div>
|
||||
|
||||
@@ -17,10 +17,18 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { 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";
|
||||
@@ -36,10 +44,9 @@ const AddRegistrySchema = z.object({
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
registryUrl: z.string().min(1, {
|
||||
message: "Registry URL is required",
|
||||
}),
|
||||
registryUrl: z.string(),
|
||||
imagePrefix: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
||||
@@ -48,9 +55,9 @@ export const AddRegistry = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } = api.registry.create.useMutation();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { mutateAsync: testRegistry, isLoading } =
|
||||
api.registry.testRegistry.useMutation();
|
||||
const router = useRouter();
|
||||
const form = useForm<AddRegistry>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
@@ -58,6 +65,7 @@ export const AddRegistry = () => {
|
||||
registryUrl: "",
|
||||
imagePrefix: "",
|
||||
registryName: "",
|
||||
serverId: "",
|
||||
},
|
||||
resolver: zodResolver(AddRegistrySchema),
|
||||
});
|
||||
@@ -67,6 +75,7 @@ export const AddRegistry = () => {
|
||||
const registryUrl = form.watch("registryUrl");
|
||||
const registryName = form.watch("registryName");
|
||||
const imagePrefix = form.watch("imagePrefix");
|
||||
const serverId = form.watch("serverId");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
@@ -74,6 +83,7 @@ export const AddRegistry = () => {
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
imagePrefix: "",
|
||||
serverId: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
@@ -85,6 +95,7 @@ export const AddRegistry = () => {
|
||||
registryUrl: data.registryUrl,
|
||||
registryType: "cloud",
|
||||
imagePrefix: data.imagePrefix,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async (data) => {
|
||||
await utils.registry.all.invalidate();
|
||||
@@ -211,34 +222,77 @@ export const AddRegistry = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testRegistry({
|
||||
username: username,
|
||||
password: password,
|
||||
registryUrl: registryUrl,
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
toast.success("Registry Tested Successfully");
|
||||
} else {
|
||||
toast.error("Registry Test Failed");
|
||||
}
|
||||
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
||||
<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 registry. 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"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testRegistry({
|
||||
username: username,
|
||||
password: password,
|
||||
registryUrl: registryUrl,
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
serverId: serverId,
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to test the registry");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Registry
|
||||
</Button>
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
toast.success("Registry Tested Successfully");
|
||||
} else {
|
||||
toast.error("Registry Test Failed");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to test the registry");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Registry
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button isLoading={form.formState.isSubmitting} type="submit">
|
||||
Create
|
||||
</Button>
|
||||
|
||||
@@ -43,7 +43,7 @@ export const ShowRegistry = () => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Server className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
To create a cluster is required to set a registry.
|
||||
To create a cluster it is required to set a registry.
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||
|
||||
@@ -17,6 +17,15 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -34,10 +43,9 @@ const updateRegistry = z.object({
|
||||
message: "Username is required",
|
||||
}),
|
||||
password: z.string(),
|
||||
registryUrl: z.string().min(1, {
|
||||
message: "Registry URL is required",
|
||||
}),
|
||||
registryUrl: z.string(),
|
||||
imagePrefix: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type UpdateRegistry = z.infer<typeof updateRegistry>;
|
||||
@@ -48,6 +56,8 @@ interface Props {
|
||||
|
||||
export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
const { mutateAsync: testRegistry, isLoading } =
|
||||
api.registry.testRegistry.useMutation();
|
||||
const { data, refetch } = api.registry.one.useQuery(
|
||||
@@ -69,15 +79,19 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
username: "",
|
||||
password: "",
|
||||
registryUrl: "",
|
||||
serverId: "",
|
||||
},
|
||||
resolver: zodResolver(updateRegistry),
|
||||
});
|
||||
|
||||
console.log(form.formState.errors);
|
||||
|
||||
const password = form.watch("password");
|
||||
const username = form.watch("username");
|
||||
const registryUrl = form.watch("registryUrl");
|
||||
const registryName = form.watch("registryName");
|
||||
const imagePrefix = form.watch("imagePrefix");
|
||||
const serverId = form.watch("serverId");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -87,6 +101,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
username: data.username || "",
|
||||
password: "",
|
||||
registryUrl: data.registryUrl || "",
|
||||
serverId: "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
@@ -99,6 +114,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
username: data.username,
|
||||
registryUrl: data.registryUrl,
|
||||
imagePrefix: data.imagePrefix,
|
||||
serverId: data.serverId,
|
||||
})
|
||||
.then(async (data) => {
|
||||
toast.success("Registry Updated");
|
||||
@@ -224,13 +240,47 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter
|
||||
className={cn(
|
||||
isCloud ? "sm:justify-between " : "",
|
||||
"flex flex-row w-full gap-4 flex-wrap",
|
||||
)}
|
||||
>
|
||||
{isCloud && (
|
||||
<DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
|
||||
<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 registry. 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"}
|
||||
@@ -243,6 +293,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
serverId: serverId,
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
@@ -258,12 +309,12 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
|
||||
>
|
||||
Test Registry
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
form="hook-form"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ShowDestinations = () => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<FolderUp className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To create a backup is required to set at least 1 provider.
|
||||
To create a backup it is required to set at least 1 provider.
|
||||
</span>
|
||||
<AddDestination />
|
||||
</div>
|
||||
|
||||
@@ -11,13 +11,11 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { format } from "date-fns";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const AddGithubProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const [manifest, setManifest] = useState("");
|
||||
const [isOrganization, setIsOrganization] = useState(false);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const ShowNotifications = () => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<BellRing className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To send notifications is required to set at least 1 provider.
|
||||
To send notifications it is required to set at least 1 provider.
|
||||
</span>
|
||||
<AddNotification />
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ProfileForm = () => {
|
||||
<div>
|
||||
<CardTitle className="text-xl">Account</CardTitle>
|
||||
<CardDescription>
|
||||
Change your details of your profile here.
|
||||
Change the details of your profile here.
|
||||
</CardDescription>
|
||||
</div>
|
||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||
@@ -145,7 +145,6 @@ export const ProfileForm = () => {
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={(e) => {
|
||||
console.log(e);
|
||||
field.onChange(e);
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
|
||||
@@ -212,7 +212,21 @@ export const AddServer = () => {
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="22" {...field} />
|
||||
<Input
|
||||
placeholder="22"
|
||||
{...field}
|
||||
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>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
@@ -228,7 +228,21 @@ export const UpdateServer = ({ serverId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="22" {...field} />
|
||||
<Input
|
||||
placeholder="22"
|
||||
{...field}
|
||||
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>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -29,11 +29,22 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addServerDomain = z.object({
|
||||
domain: z.string().min(1, { message: "URL is required" }),
|
||||
letsEncryptEmail: z.string().min(1, "Email is required").email(),
|
||||
certificateType: z.enum(["letsencrypt", "none"]),
|
||||
});
|
||||
const addServerDomain = z
|
||||
.object({
|
||||
domain: z.string().min(1, { message: "URL is required" }),
|
||||
letsEncryptEmail: z.string(),
|
||||
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>;
|
||||
|
||||
@@ -80,7 +91,7 @@ export const WebDomain = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Server Domain</CardTitle>
|
||||
<CardDescription>
|
||||
Add your server domain to your application
|
||||
Add a domain to your server application.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AddProject } from "@/components/dashboard/projects/add";
|
||||
import type { Auth } from "@/server/api/services/auth";
|
||||
import type { User } from "@/server/api/services/user";
|
||||
import { api } from "@/utils/api";
|
||||
import type { Auth, IS_CLOUD, User } from "@dokploy/server";
|
||||
import { is } from "drizzle-orm";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
@@ -11,6 +11,7 @@ interface TabInfo {
|
||||
tabLabel?: string;
|
||||
description: string;
|
||||
index: string;
|
||||
type: TabState;
|
||||
isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean;
|
||||
}
|
||||
|
||||
@@ -22,47 +23,65 @@ export type TabState =
|
||||
| "requests"
|
||||
| "docker";
|
||||
|
||||
const tabMap: Record<TabState, TabInfo> = {
|
||||
projects: {
|
||||
label: "Projects",
|
||||
description: "Manage your projects",
|
||||
index: "/dashboard/projects",
|
||||
},
|
||||
monitoring: {
|
||||
label: "Monitoring",
|
||||
description: "Monitor your projects",
|
||||
index: "/dashboard/monitoring",
|
||||
},
|
||||
traefik: {
|
||||
label: "Traefik",
|
||||
tabLabel: "Traefik File System",
|
||||
description: "Manage your traefik",
|
||||
index: "/dashboard/traefik",
|
||||
isShow: ({ rol, user }) => {
|
||||
return Boolean(rol === "admin" || user?.canAccessToTraefikFiles);
|
||||
const getTabMaps = (isCloud: boolean) => {
|
||||
const elements: TabInfo[] = [
|
||||
{
|
||||
label: "Projects",
|
||||
description: "Manage your projects",
|
||||
index: "/dashboard/projects",
|
||||
type: "projects",
|
||||
},
|
||||
},
|
||||
docker: {
|
||||
label: "Docker",
|
||||
description: "Manage your docker",
|
||||
index: "/dashboard/docker",
|
||||
isShow: ({ rol, user }) => {
|
||||
return Boolean(rol === "admin" || user?.canAccessToDocker);
|
||||
},
|
||||
},
|
||||
requests: {
|
||||
label: "Requests",
|
||||
description: "Manage your requests",
|
||||
index: "/dashboard/requests",
|
||||
isShow: ({ rol, user }) => {
|
||||
return Boolean(rol === "admin" || user?.canAccessToDocker);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
];
|
||||
|
||||
if (!isCloud) {
|
||||
elements.push(
|
||||
{
|
||||
label: "Monitoring",
|
||||
description: "Monitor your projects",
|
||||
index: "/dashboard/monitoring",
|
||||
type: "monitoring",
|
||||
},
|
||||
{
|
||||
label: "Traefik",
|
||||
tabLabel: "Traefik File System",
|
||||
description: "Manage your traefik",
|
||||
index: "/dashboard/traefik",
|
||||
isShow: ({ rol, user }) => {
|
||||
return Boolean(rol === "admin" || user?.canAccessToTraefikFiles);
|
||||
},
|
||||
type: "traefik",
|
||||
},
|
||||
{
|
||||
label: "Docker",
|
||||
description: "Manage your docker",
|
||||
index: "/dashboard/docker",
|
||||
isShow: ({ rol, user }) => {
|
||||
return Boolean(rol === "admin" || user?.canAccessToDocker);
|
||||
},
|
||||
type: "docker",
|
||||
},
|
||||
{
|
||||
label: "Requests",
|
||||
description: "Manage your requests",
|
||||
index: "/dashboard/requests",
|
||||
isShow: ({ rol, user }) => {
|
||||
return Boolean(rol === "admin" || user?.canAccessToDocker);
|
||||
},
|
||||
type: "requests",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
elements.push({
|
||||
label: "Settings",
|
||||
description: "Manage your settings",
|
||||
index: "/dashboard/settings/server",
|
||||
},
|
||||
type: "settings",
|
||||
index: isCloud
|
||||
? "/dashboard/settings/profile"
|
||||
: "/dashboard/settings/server",
|
||||
});
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@@ -72,9 +91,10 @@ interface Props {
|
||||
|
||||
export const NavigationTabs = ({ tab, children }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const [activeTab, setActiveTab] = useState<TabState>(tab);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]);
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
@@ -89,7 +109,7 @@ export const NavigationTabs = ({ tab, children }: Props) => {
|
||||
}, [tab]);
|
||||
|
||||
const activeTabInfo = useMemo(() => {
|
||||
return tabMap[activeTab];
|
||||
return tabMap.find((tab) => tab.type === activeTab);
|
||||
}, [activeTab]);
|
||||
|
||||
return (
|
||||
@@ -97,10 +117,10 @@ export const NavigationTabs = ({ tab, children }: Props) => {
|
||||
<header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-xl font-bold lg:text-3xl">
|
||||
{activeTabInfo.label}
|
||||
{activeTabInfo?.label}
|
||||
</h1>
|
||||
<p className="lg:text-medium text-muted-foreground">
|
||||
{activeTabInfo.description}
|
||||
{activeTabInfo?.description}
|
||||
</p>
|
||||
</div>
|
||||
{tab === "projects" &&
|
||||
@@ -112,27 +132,26 @@ export const NavigationTabs = ({ tab, children }: Props) => {
|
||||
className="w-full"
|
||||
onValueChange={async (e) => {
|
||||
setActiveTab(e as TabState);
|
||||
router.push(tabMap[e as TabState].index);
|
||||
const tab = tabMap.find((tab) => tab.type === e);
|
||||
router.push(tab?.index || "");
|
||||
}}
|
||||
>
|
||||
{/* className="grid w-fit grid-cols-4 bg-transparent" */}
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto border-b border-b-divider pb-1">
|
||||
<TabsList className="bg-transparent relative px-0">
|
||||
{Object.keys(tabMap).map((key) => {
|
||||
const tab = tabMap[key as TabState];
|
||||
if (tab.isShow && !tab.isShow?.({ rol: data?.rol, user })) {
|
||||
{tabMap.map((tab, index) => {
|
||||
if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={key}
|
||||
value={key}
|
||||
key={tab.type}
|
||||
value={tab.type}
|
||||
className="relative py-2.5 md:px-5 data-[state=active]:shadow-none data-[state=active]:bg-transparent rounded-md hover:bg-zinc-100 hover:dark:bg-zinc-800 data-[state=active]:hover:bg-zinc-100 data-[state=active]:hover:dark:bg-zinc-800"
|
||||
>
|
||||
<span className="relative z-[1] w-full">
|
||||
{tab.tabLabel || tab.label}
|
||||
{tab?.tabLabel || tab?.label}
|
||||
</span>
|
||||
{key === activeTab && (
|
||||
{tab.type === activeTab && (
|
||||
<div className="absolute -bottom-[5.5px] w-full">
|
||||
<div className="h-0.5 bg-foreground rounded-t-md" />
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ interface Props {
|
||||
|
||||
export const SettingsLayout = ({ children }: Props) => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
@@ -17,7 +18,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
<div className="md:max-w-[18rem] w-full">
|
||||
<Nav
|
||||
links={[
|
||||
...(data?.rol === "admin"
|
||||
...(data?.rol === "admin" && !isCloud
|
||||
? [
|
||||
{
|
||||
title: "Server",
|
||||
@@ -47,6 +48,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: Database,
|
||||
href: "/dashboard/settings/destinations",
|
||||
},
|
||||
|
||||
{
|
||||
title: "Certificates",
|
||||
label: "",
|
||||
@@ -60,7 +62,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
href: "/dashboard/settings/ssh-keys",
|
||||
},
|
||||
{
|
||||
title: "Git ",
|
||||
title: "Git",
|
||||
label: "",
|
||||
icon: GitBranch,
|
||||
href: "/dashboard/settings/git-providers",
|
||||
@@ -71,12 +73,23 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: Users,
|
||||
href: "/dashboard/settings/users",
|
||||
},
|
||||
|
||||
{
|
||||
title: "Cluster",
|
||||
title: "Registry",
|
||||
label: "",
|
||||
icon: BoxesIcon,
|
||||
href: "/dashboard/settings/cluster",
|
||||
icon: ListMusic,
|
||||
href: "/dashboard/settings/registry",
|
||||
},
|
||||
...(!isCloud
|
||||
? [
|
||||
{
|
||||
title: "Cluster",
|
||||
label: "",
|
||||
icon: BoxesIcon,
|
||||
href: "/dashboard/settings/cluster",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Notifications",
|
||||
label: "",
|
||||
@@ -128,6 +141,7 @@ import {
|
||||
GitBranch,
|
||||
KeyIcon,
|
||||
KeyRound,
|
||||
ListMusic,
|
||||
type LucideIcon,
|
||||
Route,
|
||||
Server,
|
||||
|
||||
@@ -31,4 +31,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, errorMessage, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
className={cn("text-left", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
value={props.value === undefined ? undefined : String(props.value)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
props.onChange?.(e);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
const syntheticEvent = {
|
||||
...e,
|
||||
target: {
|
||||
...e.target,
|
||||
value: number,
|
||||
},
|
||||
};
|
||||
props.onChange?.(
|
||||
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
NumberInput.displayName = "NumberInput";
|
||||
|
||||
export { Input, NumberInput };
|
||||
|
||||
1
apps/dokploy/drizzle/0038_rapid_landau.sql
Normal file
1
apps/dokploy/drizzle/0038_rapid_landau.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "registry" ALTER COLUMN "registryUrl" SET DEFAULT '';
|
||||
25
apps/dokploy/drizzle/0039_many_tiger_shark.sql
Normal file
25
apps/dokploy/drizzle/0039_many_tiger_shark.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_authId_auth_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "adminId" text;--> statement-breakpoint
|
||||
ALTER TABLE "ssh-key" ADD COLUMN "privateKey" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "ssh-key" ADD COLUMN "adminId" text;--> statement-breakpoint
|
||||
ALTER TABLE "git_provider" ADD COLUMN "adminId" text;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "notification" ADD CONSTRAINT "notification_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "git_provider" DROP COLUMN IF EXISTS "authId";
|
||||
13
apps/dokploy/drizzle/0040_graceful_wolfsbane.sql
Normal file
13
apps/dokploy/drizzle/0040_graceful_wolfsbane.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE "certificate" ADD COLUMN "adminId" text;--> statement-breakpoint
|
||||
ALTER TABLE "certificate" ADD COLUMN "serverId" text;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
3824
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
3824
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3870
apps/dokploy/drizzle/meta/0039_snapshot.json
Normal file
3870
apps/dokploy/drizzle/meta/0039_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3909
apps/dokploy/drizzle/meta/0040_snapshot.json
Normal file
3909
apps/dokploy/drizzle/meta/0040_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -267,6 +267,27 @@
|
||||
"when": 1726988289562,
|
||||
"tag": "0037_legal_namor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 38,
|
||||
"version": "6",
|
||||
"when": 1727942090102,
|
||||
"tag": "0038_rapid_landau",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 39,
|
||||
"version": "6",
|
||||
"when": 1728021127765,
|
||||
"tag": "0039_many_tiger_shark",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 40,
|
||||
"version": "6",
|
||||
"when": 1728780577084,
|
||||
"tag": "0040_graceful_wolfsbane",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -18,6 +18,7 @@ const nextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
transpilePackages: ["@dokploy/server"],
|
||||
webpack: (config) => {
|
||||
config.plugins.push(
|
||||
new CopyWebpackPlugin({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user