mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
467acc4d4d | ||
|
|
fc2778db35 | ||
|
|
85d6ff9012 | ||
|
|
522f8baec7 | ||
|
|
22eb965919 | ||
|
|
c0746b95b3 | ||
|
|
01e5cf0852 | ||
|
|
8faa6ae1cf | ||
|
|
76ed1107c2 | ||
|
|
cff5049096 | ||
|
|
cb5ca100a6 | ||
|
|
03d1e974dd | ||
|
|
b609d72d1c | ||
|
|
d7071fba60 | ||
|
|
76585991ec | ||
|
|
da6efcf733 | ||
|
|
64b0770cfb | ||
|
|
1ec83a3236 | ||
|
|
df9fad088f | ||
|
|
7d5a660f4d | ||
|
|
f7f0cbf318 | ||
|
|
841c0731aa | ||
|
|
137cd25267 | ||
|
|
4dcd16c41e | ||
|
|
60497fe59d | ||
|
|
8536945a60 | ||
|
|
e0a8d8258c | ||
|
|
fe19cdb5e4 | ||
|
|
c4654a9619 | ||
|
|
0a123a652b | ||
|
|
5d437c29b2 | ||
|
|
53f345ab1d | ||
|
|
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 | ||
|
|
2ae7e562bb | ||
|
|
e4b998c608 | ||
|
|
9b7aacc934 | ||
|
|
7027f39c48 | ||
|
|
0aff344bc0 | ||
|
|
4715f34e15 | ||
|
|
59386ed4b7 | ||
|
|
5b0bf99cbf | ||
|
|
8e227a3286 | ||
|
|
869e58739f | ||
|
|
f8721d3e04 | ||
|
|
a6c7c3b031 |
@@ -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
|
||||
|
||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
||||
npx commitlint --edit "$1"
|
||||
6
.husky/install.mjs
Normal file
6
.husky/install.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Skip Husky install in production and CI
|
||||
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
||||
process.exit(0);
|
||||
}
|
||||
const husky = (await import("husky")).default;
|
||||
console.log(husky());
|
||||
2
.husky/pre-commit
Normal file
2
.husky/pre-commit
Normal file
@@ -0,0 +1,2 @@
|
||||
pnpm run check
|
||||
git add .
|
||||
@@ -71,6 +71,12 @@ Run the command that will spin up all the required services and files.
|
||||
pnpm run dokploy:setup
|
||||
```
|
||||
|
||||
Build the server package (If you make any changes after in the packages/server folder, you need to rebuild and run this command)
|
||||
|
||||
```bash
|
||||
pnpm run server:build
|
||||
```
|
||||
|
||||
Now run the development server.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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
|
||||
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
||||
|
||||
## Additional Terms for Specific Features
|
||||
|
||||
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||
|
||||
For further inquiries or permissions, please contact us directly.
|
||||
|
||||
13
README.md
13
README.md
@@ -30,8 +30,9 @@ Dokploy include multiples features to make your life easier.
|
||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||
- **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
||||
- **CLI/API**: Manage your applications and databases using the command line or through 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}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user