mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
536a6ba2ff | ||
|
|
95ab755253 | ||
|
|
b9d6fdafac | ||
|
|
7999a4bdda | ||
|
|
50da20907f | ||
|
|
a773cfffa5 | ||
|
|
648386281f | ||
|
|
3674f3a4d6 | ||
|
|
1fea9dcf29 | ||
|
|
fb2a4d91e1 | ||
|
|
3e1063306f | ||
|
|
4d2354df47 | ||
|
|
7104fb0461 | ||
|
|
fbbbebbbd0 | ||
|
|
d18e315a28 | ||
|
|
d47efec45f | ||
|
|
4035c9a08d | ||
|
|
c5f3c61275 | ||
|
|
9436477f41 | ||
|
|
43d48520be | ||
|
|
1d1a3cede1 | ||
|
|
213fa08210 | ||
|
|
1eed1a356d | ||
|
|
a8f21ad717 | ||
|
|
c1420bd6d8 | ||
|
|
74ea9debd5 | ||
|
|
da8955dabb | ||
|
|
5dbf18605f | ||
|
|
1df9f1f4df | ||
|
|
f06ac587c9 | ||
|
|
c084cf84a0 | ||
|
|
5e5cbdeef9 | ||
|
|
24929d8a4d | ||
|
|
1e6e85ed5b | ||
|
|
137edf1250 | ||
|
|
b8741f1702 | ||
|
|
ac28aff022 | ||
|
|
217be3c6e9 | ||
|
|
27a0dc3770 | ||
|
|
53b24534a8 | ||
|
|
5a3d0f8288 | ||
|
|
ff3e3513ef | ||
|
|
7a1fba38b3 | ||
|
|
2b65a3c119 | ||
|
|
6d841497cc | ||
|
|
d37dc7c372 | ||
|
|
f3e0cf861f | ||
|
|
e2578e5794 | ||
|
|
16deec381c | ||
|
|
91dc35a138 | ||
|
|
acfa032e61 | ||
|
|
83ee4b2c59 | ||
|
|
d5c6a601d8 | ||
|
|
afbe42a577 | ||
|
|
866f700abf | ||
|
|
f2b6b33b1f | ||
|
|
813ffabb8c | ||
|
|
b5da9291b4 | ||
|
|
b0d604d12b | ||
|
|
e9f40e1644 | ||
|
|
61d520c239 | ||
|
|
124a884d2e | ||
|
|
b5e4b9af60 | ||
|
|
840c24e3ca | ||
|
|
d300eb73fb | ||
|
|
fb4e06116c | ||
|
|
2d3b903edc | ||
|
|
75c13df22f | ||
|
|
68b81cb48d | ||
|
|
9d6f2df25a | ||
|
|
452793c8e5 | ||
|
|
724de2c1b9 | ||
|
|
86946b6b15 | ||
|
|
957bb3d3e6 | ||
|
|
38a75b07fb | ||
|
|
378b93f996 | ||
|
|
eb62d124bd | ||
|
|
7558029271 | ||
|
|
757c28dad1 | ||
|
|
8d3dc38816 | ||
|
|
9c8061a447 | ||
|
|
3a8b2867b6 | ||
|
|
389956d1a2 | ||
|
|
bf6ed15ba7 | ||
|
|
31a66ce798 | ||
|
|
38c1d86e2f | ||
|
|
d08e232f50 | ||
|
|
3d49383c42 | ||
|
|
27706eaae4 | ||
|
|
c74b5a2677 | ||
|
|
0374165a7f | ||
|
|
b7dad5e1d9 | ||
|
|
65527bc39a | ||
|
|
a84bdd1c8e | ||
|
|
eb219221be | ||
|
|
7b176bd877 | ||
|
|
6970923253 | ||
|
|
a3e23d54d8 | ||
|
|
8f11207d72 | ||
|
|
6bd98350d9 | ||
|
|
096ef8cd93 | ||
|
|
d6eafcbb9b | ||
|
|
c0261384ca | ||
|
|
ca733addc2 | ||
|
|
7497671033 | ||
|
|
385fbf4af5 | ||
|
|
44e75ee7e1 | ||
|
|
6b4d6eac1d | ||
|
|
9379d4a31d | ||
|
|
dde799f510 | ||
|
|
ecb919e109 | ||
|
|
29ca894a97 | ||
|
|
84ba74a673 | ||
|
|
32b0d51e79 | ||
|
|
1288660fd6 | ||
|
|
5c1e24f4f3 | ||
|
|
3e12e1b1b3 | ||
|
|
175e84f50e | ||
|
|
efb646c43d | ||
|
|
fa950dae39 | ||
|
|
712ad25e7a | ||
|
|
35a41e774e | ||
|
|
c2ac193fbe | ||
|
|
ce3c89a715 | ||
|
|
96f7206a1d | ||
|
|
b7ace886f3 | ||
|
|
5dc330eaa3 | ||
|
|
b7f5bee2f8 | ||
|
|
19ee5f073b | ||
|
|
1fd4a6ae80 | ||
|
|
3c8a412014 | ||
|
|
eee617719b | ||
|
|
fc611946a6 | ||
|
|
bd84793780 | ||
|
|
9922c0ed66 | ||
|
|
af13c84968 | ||
|
|
ddb78ef8dd | ||
|
|
3590f3bed2 | ||
|
|
c70089ee53 | ||
|
|
161e479a0b | ||
|
|
bd735bfb64 | ||
|
|
85c814620e | ||
|
|
fb013fe4ec | ||
|
|
90a1bd9027 | ||
|
|
610ef8f35c | ||
|
|
9b2fcaea31 | ||
|
|
2ffa95b9fa | ||
|
|
559872c4e4 | ||
|
|
85642ed5f2 | ||
|
|
8bf701d2f2 | ||
|
|
38809a2034 | ||
|
|
4bd6ec2232 | ||
|
|
95899b7208 | ||
|
|
ac26bb95e3 | ||
|
|
5abcc82215 | ||
|
|
16791a9f4b | ||
|
|
54ab6e3436 | ||
|
|
f5ca72ddd7 | ||
|
|
7245e7dfd7 | ||
|
|
547d149987 | ||
|
|
3b2d29514c | ||
|
|
115abd378f | ||
|
|
9c101d78d1 | ||
|
|
610f8fa5bc | ||
|
|
e201bf12f8 | ||
|
|
bf872200a7 | ||
|
|
abc6906349 | ||
|
|
9440fd89ae | ||
|
|
bce1eb8907 | ||
|
|
4bf44b3275 | ||
|
|
06355ff089 | ||
|
|
95ecf4fe21 | ||
|
|
d50a6ce76f | ||
|
|
2d951e0b1f | ||
|
|
416de9879b | ||
|
|
082aff58a9 | ||
|
|
b74666fc2f | ||
|
|
dc626f1a94 | ||
|
|
533a5e490f | ||
|
|
cf54e4f5c2 | ||
|
|
d84c808887 | ||
|
|
89cd35adc6 | ||
|
|
6299385bb4 | ||
|
|
e6f9867500 | ||
|
|
3fdd3ddc74 | ||
|
|
6ed379243e | ||
|
|
27256c609a | ||
|
|
c4d59177bf | ||
|
|
3c8ca2b012 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,6 +35,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -166,20 +166,26 @@ import {
|
|||||||
generateRandomDomain,
|
generateRandomDomain,
|
||||||
type Template,
|
type Template,
|
||||||
type Schema,
|
type Schema,
|
||||||
|
type DomainSchema,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
export function generate(schema: Schema): Template {
|
export function generate(schema: Schema): Template {
|
||||||
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
||||||
const mainServiceHash = generateHash(schema.projectName);
|
const mainServiceHash = generateHash(schema.projectName);
|
||||||
const randomDomain = generateRandomDomain(schema);
|
const mainDomain = generateRandomDomain(schema);
|
||||||
const secretBase = generateBase64(64);
|
const secretBase = generateBase64(64);
|
||||||
const toptKeyBase = generateBase64(32);
|
const toptKeyBase = generateBase64(32);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 8000,
|
||||||
|
serviceName: "plausible",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const envs = [
|
const envs = [
|
||||||
// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
`BASE_URL=http://${mainDomain}`,
|
||||||
`PLAUSIBLE_HOST=${randomDomain}`,
|
|
||||||
"PLAUSIBLE_PORT=8000",
|
|
||||||
`BASE_URL=http://${randomDomain}`,
|
|
||||||
`SECRET_KEY_BASE=${secretBase}`,
|
`SECRET_KEY_BASE=${secretBase}`,
|
||||||
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
||||||
`HASH=${mainServiceHash}`,
|
`HASH=${mainServiceHash}`,
|
||||||
@@ -195,6 +201,7 @@ export function generate(schema: Schema): Template {
|
|||||||
return {
|
return {
|
||||||
envs,
|
envs,
|
||||||
mounts,
|
mounts,
|
||||||
|
domains,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
# Install buildpacks
|
# Install buildpacks
|
||||||
RUN curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD [ "pnpm", "start" ]
|
CMD [ "pnpm", "start" ]
|
||||||
|
|||||||
26
LICENSE.MD
Normal file
26
LICENSE.MD
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# License
|
||||||
|
|
||||||
|
## Core License (Apache License 2.0)
|
||||||
|
|
||||||
|
Copyright 2024 Mauricio Siu.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
For further inquiries or permissions, please contact us directly.
|
||||||
11
README.md
11
README.md
@@ -1,7 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h1 align="center">Dokploy</h1>
|
<h1 align="center">Dokploy</h1>
|
||||||
<div>
|
<div>
|
||||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
|
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://dokploy.com/og.png" >
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<div>Join us on Discord for help, feedback, and discussions!</div>
|
<div>Join us on Discord for help, feedback, and discussions!</div>
|
||||||
</br>
|
</br>
|
||||||
<a href="https://discord.gg/ZXwG32bw">
|
<a href="https://discord.gg/2tBnJ3jDJc">
|
||||||
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
|
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +59,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://supafort.com/" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
|
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Elite Contributors 🥈 -->
|
<!-- Elite Contributors 🥈 -->
|
||||||
@@ -69,13 +69,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
### Supporting Members 🥉
|
### Supporting Members 🥉
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://lightspeed.run/"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||||
|
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Lightspeed.run"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://steamsets.com/"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
#### Organizations:
|
#### Organizations:
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ title: "Overview"
|
|||||||
description: "Learn how to use Docker Compose with Dokploy"
|
description: "Learn how to use Docker Compose with Dokploy"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
import { Callout } from "fumadocs-ui/components/callout";
|
||||||
|
|
||||||
Dokploy integrates with Docker Compose and Docker Stack to provide flexible deployment solutions. Whether you are developing locally or deploying at scale, Dokploy facilitates application management through these powerful Docker tools.
|
Dokploy integrates with Docker Compose and Docker Stack to provide flexible deployment solutions. Whether you are developing locally or deploying at scale, Dokploy facilitates application management through these powerful Docker tools.
|
||||||
|
|
||||||
### Configuration Methods
|
### Configuration Methods
|
||||||
|
|
||||||
Dokploy provides two methods for creating Docker Compose configurations:
|
Dokploy provides two methods for creating Docker Compose configurations:
|
||||||
|
|
||||||
|
|
||||||
- **Docker Compose**: Ideal for standard Docker Compose configurations.
|
- **Docker Compose**: Ideal for standard Docker Compose configurations.
|
||||||
- **Stack**: Geared towards orchestrating applications using Docker Swarm. Note that some Docker Compose features, such as `build`, are not available in this mode.
|
- **Stack**: Geared towards orchestrating applications using Docker Swarm. Note that some Docker Compose features, such as `build`, are not available in this mode.
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ Configure the source of your code, the way your application is built, and also m
|
|||||||
|
|
||||||
A code editor within Dokploy allows you to specify environment variables for your Docker Compose file. By default, Dokploy creates a `.env` file in the specified Docker Compose file path.
|
A code editor within Dokploy allows you to specify environment variables for your Docker Compose file. By default, Dokploy creates a `.env` file in the specified Docker Compose file path.
|
||||||
|
|
||||||
### Monitoring
|
### Monitoring
|
||||||
|
|
||||||
Monitor each service individually within Dokploy. If your application consists of multiple services, each can be monitored separately to ensure optimal performance.
|
Monitor each service individually within Dokploy. If your application consists of multiple services, each can be monitored separately to ensure optimal performance.
|
||||||
|
|
||||||
@@ -29,7 +30,6 @@ Monitor each service individually within Dokploy. If your application consists o
|
|||||||
|
|
||||||
Access detailed logs for each service through the Dokploy log viewer, which can help in troubleshooting and ensuring the stability of your services.
|
Access detailed logs for each service through the Dokploy log viewer, which can help in troubleshooting and ensuring the stability of your services.
|
||||||
|
|
||||||
|
|
||||||
### Deployments
|
### Deployments
|
||||||
|
|
||||||
You can view the last 10 deployments of your application. When you deploy your application in real time, a new deployment record will be created and it will gradually show you how your application is being built.
|
You can view the last 10 deployments of your application. When you deploy your application in real time, a new deployment record will be created and it will gradually show you how your application is being built.
|
||||||
@@ -38,7 +38,6 @@ We also offer a button to cancel deployments that are in queue. Note that those
|
|||||||
|
|
||||||
We provide a webhook so that you can trigger your own deployments by pushing to your GitHub, Gitea, GitLab, Bitbucket repository.
|
We provide a webhook so that you can trigger your own deployments by pushing to your GitHub, Gitea, GitLab, Bitbucket repository.
|
||||||
|
|
||||||
|
|
||||||
### Advanced
|
### Advanced
|
||||||
|
|
||||||
This section provides advanced configuration options for experienced users. It includes tools for custom commands within the container and volumes.
|
This section provides advanced configuration options for experienced users. It includes tools for custom commands within the container and volumes.
|
||||||
@@ -46,4 +45,32 @@ This section provides advanced configuration options for experienced users. It i
|
|||||||
- **Command**: Dokploy has a defined command to run the Docker Compose file, ensuring complete control through the UI. However, you can append flags or options to the command.
|
- **Command**: Dokploy has a defined command to run the Docker Compose file, ensuring complete control through the UI. However, you can append flags or options to the command.
|
||||||
- **Volumes**: To ensure data persistence across deployments, configure storage volumes for your application.
|
- **Volumes**: To ensure data persistence across deployments, configure storage volumes for your application.
|
||||||
|
|
||||||
<ImageZoom src="/assets/images/compose/overview.png" width={800} height={630} quality={100} priority alt='home og image' className="rounded-lg" />
|
<ImageZoom
|
||||||
|
src="/assets/images/compose/overview.png"
|
||||||
|
width={800}
|
||||||
|
height={630}
|
||||||
|
quality={100}
|
||||||
|
priority
|
||||||
|
alt="home og image"
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Callout title="Volumes">
|
||||||
|
Docker volumes are a way to persist data generated and used by Docker containers. They are particularly useful for maintaining data between container restarts or for sharing data among different containers.
|
||||||
|
|
||||||
|
To bind a volume to the host machine, you can use the following syntax in your docker-compose.yml file, but this way will clean up the volumes when a new deployment is made:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- "/folder:/path/in/container" ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
It's recommended to use the ../files folder to ensure your data persists between deployments. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- "../files/my-database:/var/lib/mysql" ✅
|
||||||
|
- "../files/my-configs:/etc/my-app/config" ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
</Callout>
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ The following templates are available:
|
|||||||
- **Metabase**: Open Source Business Intelligence
|
- **Metabase**: Open Source Business Intelligence
|
||||||
- **Grafana**: Open Source Dashboard for your metrics
|
- **Grafana**: Open Source Dashboard for your metrics
|
||||||
- **Wordpress**: Open Source Content Management System
|
- **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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -42,4 +43,4 @@ We accept contributions to upload new templates to the dokploy repository.
|
|||||||
|
|
||||||
Make sure to follow the guidelines for creating a template:
|
Make sure to follow the guidelines for creating a template:
|
||||||
|
|
||||||
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
|
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fumadocs-core": "12.2.2",
|
"fumadocs-core": "^12.5.6",
|
||||||
"fumadocs-mdx": "8.2.33",
|
"fumadocs-mdx": "^8.2.34",
|
||||||
"fumadocs-openapi": "^3.1.3",
|
"fumadocs-openapi": "^3.3.0",
|
||||||
"fumadocs-ui": "12.2.2",
|
"fumadocs-ui": "^12.5.6",
|
||||||
"lucide-react": "^0.394.0",
|
"lucide-react": "^0.394.0",
|
||||||
"next": "^14.2.4",
|
"next": "^14.2.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
89
apps/dokploy/__test__/compose/domain/labels.test.ts
Normal file
89
apps/dokploy/__test__/compose/domain/labels.test.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Domain } from "@/server/api/services/domain";
|
||||||
|
import { createDomainLabels } from "@/server/utils/docker/domain";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("createDomainLabels", () => {
|
||||||
|
const appName = "test-app";
|
||||||
|
const baseDomain: Domain = {
|
||||||
|
host: "example.com",
|
||||||
|
port: 8080,
|
||||||
|
https: false,
|
||||||
|
uniqueConfigKey: 1,
|
||||||
|
certificateType: "none",
|
||||||
|
applicationId: "",
|
||||||
|
composeId: "",
|
||||||
|
domainType: "compose",
|
||||||
|
serviceName: "test-app",
|
||||||
|
domainId: "",
|
||||||
|
path: "/",
|
||||||
|
createdAt: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should create basic labels for web entrypoint", async () => {
|
||||||
|
const labels = await createDomainLabels(appName, baseDomain, "web");
|
||||||
|
expect(labels).toEqual([
|
||||||
|
"traefik.http.routers.test-app-1-web.rule=Host(`example.com`)",
|
||||||
|
"traefik.http.routers.test-app-1-web.entrypoints=web",
|
||||||
|
"traefik.http.services.test-app-1-web.loadbalancer.server.port=8080",
|
||||||
|
"traefik.http.routers.test-app-1-web.service=test-app-1-web",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create labels for websecure entrypoint", async () => {
|
||||||
|
const labels = await createDomainLabels(appName, baseDomain, "websecure");
|
||||||
|
expect(labels).toEqual([
|
||||||
|
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`)",
|
||||||
|
"traefik.http.routers.test-app-1-websecure.entrypoints=websecure",
|
||||||
|
"traefik.http.services.test-app-1-websecure.loadbalancer.server.port=8080",
|
||||||
|
"traefik.http.routers.test-app-1-websecure.service=test-app-1-websecure",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add redirect middleware for https on web entrypoint", async () => {
|
||||||
|
const httpsBaseDomain = { ...baseDomain, https: true };
|
||||||
|
const labels = await createDomainLabels(appName, httpsBaseDomain, "web");
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add Let's Encrypt configuration for websecure with letsencrypt certificate", async () => {
|
||||||
|
const letsencryptDomain = {
|
||||||
|
...baseDomain,
|
||||||
|
https: true,
|
||||||
|
certificateType: "letsencrypt" as const,
|
||||||
|
};
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
letsencryptDomain,
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add Let's Encrypt configuration for non-letsencrypt certificate", async () => {
|
||||||
|
const nonLetsencryptDomain = {
|
||||||
|
...baseDomain,
|
||||||
|
https: true,
|
||||||
|
certificateType: "none" as const,
|
||||||
|
};
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
nonLetsencryptDomain,
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
expect(labels).not.toContain(
|
||||||
|
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle different ports correctly", async () => {
|
||||||
|
const customPortDomain = { ...baseDomain, port: 3000 };
|
||||||
|
const labels = await createDomainLabels(appName, customPortDomain, "web");
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
29
apps/dokploy/__test__/compose/domain/network-root.test.ts
Normal file
29
apps/dokploy/__test__/compose/domain/network-root.test.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("addDokployNetworkToRoot", () => {
|
||||||
|
it("should create network object if networks is undefined", () => {
|
||||||
|
const result = addDokployNetworkToRoot(undefined);
|
||||||
|
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add network to an empty object", () => {
|
||||||
|
const result = addDokployNetworkToRoot({});
|
||||||
|
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not modify existing network configuration", () => {
|
||||||
|
const existing = { "dokploy-network": { external: false } };
|
||||||
|
const result = addDokployNetworkToRoot(existing);
|
||||||
|
expect(result).toEqual({ "dokploy-network": { external: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add network alongside existing networks", () => {
|
||||||
|
const existing = { "other-network": { external: true } };
|
||||||
|
const result = addDokployNetworkToRoot(existing);
|
||||||
|
expect(result).toEqual({
|
||||||
|
"other-network": { external: true },
|
||||||
|
"dokploy-network": { external: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
24
apps/dokploy/__test__/compose/domain/network-service.test.ts
Normal file
24
apps/dokploy/__test__/compose/domain/network-service.test.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { addDokployNetworkToService } from "@/server/utils/docker/domain";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("addDokployNetworkToService", () => {
|
||||||
|
it("should add network to an empty array", () => {
|
||||||
|
const result = addDokployNetworkToService([]);
|
||||||
|
expect(result).toEqual(["dokploy-network"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add duplicate network to an array", () => {
|
||||||
|
const result = addDokployNetworkToService(["dokploy-network"]);
|
||||||
|
expect(result).toEqual(["dokploy-network"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add network to an existing array with other networks", () => {
|
||||||
|
const result = addDokployNetworkToService(["other-network"]);
|
||||||
|
expect(result).toEqual(["other-network", "dokploy-network"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add network to an object if networks is an object", () => {
|
||||||
|
const result = addDokployNetworkToService({ "other-network": {} });
|
||||||
|
expect(result).toEqual({ "other-network": {}, "dokploy-network": {} });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -78,7 +78,7 @@ test("Should not touch config without host", () => {
|
|||||||
expect(originalConfig).toEqual(config);
|
expect(originalConfig).toEqual(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should remove web-secure if https rollback to http", () => {
|
test("Should remove websecure if https rollback to http", () => {
|
||||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||||
|
|
||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const baseApp: ApplicationNested = {
|
|||||||
placementSwarm: null,
|
placementSwarm: null,
|
||||||
ports: [],
|
ports: [],
|
||||||
projectId: "",
|
projectId: "",
|
||||||
|
publishDirectory: null,
|
||||||
redirects: [],
|
redirects: [],
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
registry: null,
|
registry: null,
|
||||||
@@ -54,6 +55,7 @@ const baseApp: ApplicationNested = {
|
|||||||
title: null,
|
title: null,
|
||||||
updateConfigSwarm: null,
|
updateConfigSwarm: null,
|
||||||
username: null,
|
username: null,
|
||||||
|
dockerContextPath: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseDomain: Domain = {
|
const baseDomain: Domain = {
|
||||||
@@ -65,6 +67,9 @@ const baseDomain: Domain = {
|
|||||||
https: false,
|
https: false,
|
||||||
path: null,
|
path: null,
|
||||||
port: null,
|
port: null,
|
||||||
|
serviceName: "",
|
||||||
|
composeId: "",
|
||||||
|
domainType: "application",
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -52,6 +52,7 @@ export const AddPort = ({
|
|||||||
applicationId,
|
applicationId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
@@ -82,6 +83,7 @@ export const AddPort = ({
|
|||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the port");
|
toast.error("Error to create the port");
|
||||||
@@ -89,7 +91,7 @@ export const AddPort = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -49,6 +49,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdatePort = ({ portId }: Props) => {
|
export const UpdatePort = ({ portId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data } = api.port.one.useQuery(
|
const { data } = api.port.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -89,6 +90,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId: response?.applicationId,
|
applicationId: response?.applicationId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the port");
|
toast.error("Error to update the port");
|
||||||
@@ -96,7 +98,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -45,6 +45,7 @@ export const AddRedirect = ({
|
|||||||
applicationId,
|
applicationId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
@@ -80,6 +81,7 @@ export const AddRedirect = ({
|
|||||||
await utils.application.readTraefikConfig.invalidate({
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the redirect");
|
toast.error("Error to create the redirect");
|
||||||
@@ -87,7 +89,7 @@ export const AddRedirect = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -41,6 +41,7 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateRedirect = ({ redirectId }: Props) => {
|
export const UpdateRedirect = ({ redirectId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data } = api.redirects.one.useQuery(
|
const { data } = api.redirects.one.useQuery(
|
||||||
{
|
{
|
||||||
redirectId,
|
redirectId,
|
||||||
@@ -84,6 +85,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
|||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId: response?.applicationId,
|
applicationId: response?.applicationId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the redirect");
|
toast.error("Error to update the redirect");
|
||||||
@@ -91,7 +93,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -43,7 +43,7 @@ export const AddSecurity = ({
|
|||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.security.create.useMutation();
|
api.security.create.useMutation();
|
||||||
|
|
||||||
@@ -72,6 +72,7 @@ export const AddSecurity = ({
|
|||||||
await utils.application.readTraefikConfig.invalidate({
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the security");
|
toast.error("Error to create the security");
|
||||||
@@ -79,7 +80,7 @@ export const AddSecurity = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -38,6 +38,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateSecurity = ({ securityId }: Props) => {
|
export const UpdateSecurity = ({ securityId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data } = api.security.one.useQuery(
|
const { data } = api.security.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -79,6 +80,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
|||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId: response?.applicationId,
|
applicationId: response?.applicationId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the security");
|
toast.error("Error to update the security");
|
||||||
@@ -86,7 +88,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
<div className="flex flex-col pt-2 relative">
|
<div className="flex flex-col pt-2 relative">
|
||||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
value={data || "Empty"}
|
value={data || "Empty"}
|
||||||
disabled
|
disabled
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Traefik config</FormLabel>
|
<FormLabel>Traefik config</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`http:
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { api } from "@/utils/api";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -77,6 +77,7 @@ export const AddVolumes = ({
|
|||||||
refetch,
|
refetch,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync } = api.mounts.create.useMutation();
|
const { mutateAsync } = api.mounts.create.useMutation();
|
||||||
const form = useForm<AddMount>({
|
const form = useForm<AddMount>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -103,6 +104,7 @@ export const AddVolumes = ({
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mount Created");
|
toast.success("Mount Created");
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the Bind mount");
|
toast.error("Error to create the Bind mount");
|
||||||
@@ -117,6 +119,7 @@ export const AddVolumes = ({
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mount Created");
|
toast.success("Mount Created");
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the Volume mount");
|
toast.error("Error to create the Volume mount");
|
||||||
@@ -132,6 +135,7 @@ export const AddVolumes = ({
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mount Created");
|
toast.success("Mount Created");
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the File mount");
|
toast.error("Error to create the File mount");
|
||||||
@@ -142,7 +146,7 @@ export const AddVolumes = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>{children}</Button>
|
<Button>{children}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
@@ -22,7 +23,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Pencil } from "lucide-react";
|
import { Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -76,6 +77,7 @@ export const UpdateVolume = ({
|
|||||||
refetch,
|
refetch,
|
||||||
serviceType,
|
serviceType,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data } = api.mounts.one.useQuery(
|
const { data } = api.mounts.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -135,6 +137,7 @@ export const UpdateVolume = ({
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mount Update");
|
toast.success("Mount Update");
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Bind mount");
|
toast.error("Error to update the Bind mount");
|
||||||
@@ -148,6 +151,7 @@ export const UpdateVolume = ({
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mount Update");
|
toast.success("Mount Update");
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Volume mount");
|
toast.error("Error to update the Volume mount");
|
||||||
@@ -162,6 +166,7 @@ export const UpdateVolume = ({
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mount Update");
|
toast.success("Mount Update");
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the File mount");
|
toast.error("Error to update the File mount");
|
||||||
@@ -171,7 +176,7 @@ export const UpdateVolume = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
<Pencil className="size-4 text-muted-foreground" />
|
||||||
@@ -291,13 +296,15 @@ export const UpdateVolume = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<DialogClose>
|
||||||
isLoading={isLoading}
|
<Button
|
||||||
form="hook-form-update-volume"
|
isLoading={isLoading}
|
||||||
type="submit"
|
form="hook-form-update-volume"
|
||||||
>
|
type="submit"
|
||||||
Update
|
>
|
||||||
</Button>
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -23,6 +24,7 @@ enum BuildType {
|
|||||||
heroku_buildpacks = "heroku_buildpacks",
|
heroku_buildpacks = "heroku_buildpacks",
|
||||||
paketo_buildpacks = "paketo_buildpacks",
|
paketo_buildpacks = "paketo_buildpacks",
|
||||||
nixpacks = "nixpacks",
|
nixpacks = "nixpacks",
|
||||||
|
static = "static",
|
||||||
}
|
}
|
||||||
|
|
||||||
const mySchema = z.discriminatedUnion("buildType", [
|
const mySchema = z.discriminatedUnion("buildType", [
|
||||||
@@ -34,6 +36,7 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
invalid_type_error: "Dockerfile path is required",
|
invalid_type_error: "Dockerfile path is required",
|
||||||
})
|
})
|
||||||
.min(1, "Dockerfile required"),
|
.min(1, "Dockerfile required"),
|
||||||
|
dockerContextPath: z.string().nullable().default(""),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("heroku_buildpacks"),
|
buildType: z.literal("heroku_buildpacks"),
|
||||||
@@ -43,6 +46,10 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("nixpacks"),
|
buildType: z.literal("nixpacks"),
|
||||||
|
publishDirectory: z.string().optional(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
buildType: z.literal("static"),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -73,17 +80,18 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
const buildType = form.watch("buildType");
|
const buildType = form.watch("buildType");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
// TODO: refactor this
|
|
||||||
if (data.buildType === "dockerfile") {
|
if (data.buildType === "dockerfile") {
|
||||||
form.reset({
|
form.reset({
|
||||||
buildType: data.buildType,
|
buildType: data.buildType,
|
||||||
...(data.buildType && {
|
...(data.buildType && {
|
||||||
dockerfile: data.dockerfile || "",
|
dockerfile: data.dockerfile || "",
|
||||||
|
dockerContextPath: data.dockerContextPath || "",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
form.reset({
|
form.reset({
|
||||||
buildType: data.buildType,
|
buildType: data.buildType,
|
||||||
|
publishDirectory: data.publishDirectory || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +101,11 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
buildType: data.buildType,
|
buildType: data.buildType,
|
||||||
|
publishDirectory:
|
||||||
|
data.buildType === "nixpacks" ? data.publishDirectory : null,
|
||||||
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
|
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
|
||||||
|
dockerContextPath:
|
||||||
|
data.buildType === "dockerfile" ? data.dockerContextPath : null,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Build type saved");
|
toast.success("Build type saved");
|
||||||
@@ -171,6 +183,12 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
Paketo Buildpacks
|
Paketo Buildpacks
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem value="static" />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="font-normal">Static</FormLabel>
|
||||||
|
</FormItem>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -179,16 +197,71 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{buildType === "dockerfile" && (
|
{buildType === "dockerfile" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerfile"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Docker File</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={"Path of your docker file"}
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerContextPath"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Docker Context Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={
|
||||||
|
"Path of your docker context default: ."
|
||||||
|
}
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{buildType === "nixpacks" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerfile"
|
name="publishDirectory"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Docker File</FormLabel>
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Publish Directory</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Allows you to serve a single directory via NGINX after
|
||||||
|
the build phase. Useful if the final build assets
|
||||||
|
should be served as a static site.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"Path of your docker file"}
|
placeholder={"Publish Directory"}
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
<span>Webhook URL: </span>
|
<span>Webhook URL: </span>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<span className="text-muted-foreground">
|
<span className="break-all text-muted-foreground">
|
||||||
{`${url}/api/deploy/${data?.refreshToken}`}
|
{`${url}/api/deploy/${data?.refreshToken}`}
|
||||||
</span>
|
</span>
|
||||||
<RefreshToken applicationId={applicationId} />
|
<RefreshToken applicationId={applicationId} />
|
||||||
@@ -72,7 +72,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
{deployments?.map((deployment) => (
|
{deployments?.map((deployment) => (
|
||||||
<div
|
<div
|
||||||
key={deployment.deploymentId}
|
key={deployment.deploymentId}
|
||||||
className="flex items-center justify-between rounded-lg border p-4"
|
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
@@ -87,7 +87,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
{deployment.title}
|
{deployment.title}
|
||||||
</span>
|
</span>
|
||||||
{deployment.description && (
|
{deployment.description && (
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="break-all text-sm text-muted-foreground">
|
||||||
{deployment.description}
|
{deployment.description}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,13 +27,20 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { domain } from "@/server/db/validations";
|
import { domain } from "@/server/db/validations/domain";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Dices } from "lucide-react";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
@@ -60,10 +67,22 @@ export const AddDomain = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: application } = api.application.one.useQuery(
|
||||||
|
{
|
||||||
|
applicationId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!applicationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } = domainId
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
? api.domain.update.useMutation()
|
? api.domain.update.useMutation()
|
||||||
: api.domain.create.useMutation();
|
: api.domain.create.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||||
|
api.domain.generateDomain.useMutation();
|
||||||
|
|
||||||
const form = useForm<Domain>({
|
const form = useForm<Domain>({
|
||||||
resolver: zodResolver(domain),
|
resolver: zodResolver(domain),
|
||||||
});
|
});
|
||||||
@@ -142,9 +161,42 @@ export const AddDomain = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Host</FormLabel>
|
<FormLabel>Host</FormLabel>
|
||||||
<FormControl>
|
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
|
||||||
<Input placeholder="api.dokploy.com" {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<Input placeholder="api.dokploy.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingGenerate}
|
||||||
|
onClick={() => {
|
||||||
|
generateDomain({
|
||||||
|
appName: application?.appName || "",
|
||||||
|
})
|
||||||
|
.then((domain) => {
|
||||||
|
field.onChange(domain);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dices className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>Generate traefik.me domain</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -44,12 +44,19 @@ export const DeleteDomain = ({ domainId }: Props) => {
|
|||||||
domainId,
|
domainId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
utils.domain.byApplicationId.invalidate({
|
if (data?.applicationId) {
|
||||||
applicationId: data?.applicationId,
|
utils.domain.byApplicationId.invalidate({
|
||||||
});
|
applicationId: data?.applicationId,
|
||||||
utils.application.readTraefikConfig.invalidate({
|
});
|
||||||
applicationId: data?.applicationId,
|
utils.application.readTraefikConfig.invalidate({
|
||||||
});
|
applicationId: data?.applicationId,
|
||||||
|
});
|
||||||
|
} else if (data?.composeId) {
|
||||||
|
utils.domain.byComposeId.invalidate({
|
||||||
|
composeId: data?.composeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toast.success("Domain delete succesfully");
|
toast.success("Domain delete succesfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { RefreshCcw } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { GenerateTraefikMe } from "./generate-traefikme";
|
|
||||||
import { GenerateWildCard } from "./generate-wildcard";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GenerateDomain = ({ applicationId }: Props) => {
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger className="" asChild>
|
|
||||||
<Button variant="secondary">
|
|
||||||
Generate Domain
|
|
||||||
<RefreshCcw className="size-4 text-muted-foreground " />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Generate Domain</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Generate Domains for your applications
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 w-full">
|
|
||||||
<ul className="flex flex-col gap-4">
|
|
||||||
<li className="flex flex-row items-center gap-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="text-base font-bold">
|
|
||||||
1. Generate TraefikMe Domain
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
This option generates a free domain provided by{" "}
|
|
||||||
<Link
|
|
||||||
href="https://traefik.me"
|
|
||||||
className="text-primary"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
TraefikMe
|
|
||||||
</Link>
|
|
||||||
. We recommend using this for quick domain testing or if you
|
|
||||||
don't have a domain yet.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{/* <li className="flex flex-row items-center gap-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="text-base font-bold">
|
|
||||||
2. Use Wildcard Domain
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
To use this option, you need to set up an 'A' record in your
|
|
||||||
domain provider. For example, create a record for
|
|
||||||
*.yourdomain.com.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li> */}
|
|
||||||
</ul>
|
|
||||||
<div className="flex flex-row gap-4 w-full">
|
|
||||||
<GenerateTraefikMe applicationId={applicationId} />
|
|
||||||
{/* <GenerateWildCard applicationId={applicationId} /> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { RefreshCcw } from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
export const GenerateTraefikMe = ({ applicationId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
return (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button variant="secondary" isLoading={isLoading}>
|
|
||||||
Generate Domain
|
|
||||||
<RefreshCcw className="size-4 text-muted-foreground " />
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
Are you sure to generate a new domain?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will generate a new domain and will be used to access to the
|
|
||||||
application
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
applicationId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
utils.domain.byApplicationId.invalidate({
|
|
||||||
applicationId: applicationId,
|
|
||||||
});
|
|
||||||
utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId: applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Generated Domain succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to generate Domain");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { SquareAsterisk } from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
export const GenerateWildCard = ({ applicationId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
return (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button variant="secondary" isLoading={isLoading}>
|
|
||||||
Generate Wildcard Domain
|
|
||||||
<SquareAsterisk className="size-4 text-muted-foreground " />
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
Are you sure to generate a new wildcard domain?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will generate a new domain and will be used to access to the
|
|
||||||
application
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
applicationId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
utils.domain.byApplicationId.invalidate({
|
|
||||||
applicationId: applicationId,
|
|
||||||
});
|
|
||||||
utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId: applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Generated Domain succesfully");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error(`Error to generate Domain: ${e.message}`);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -12,7 +12,6 @@ import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AddDomain } from "./add-domain";
|
import { AddDomain } from "./add-domain";
|
||||||
import { DeleteDomain } from "./delete-domain";
|
import { DeleteDomain } from "./delete-domain";
|
||||||
import { GenerateDomain } from "./generate-domain";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@@ -46,9 +45,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</AddDomain>
|
</AddDomain>
|
||||||
)}
|
)}
|
||||||
{data && data?.length > 0 && (
|
|
||||||
<GenerateDomain applicationId={applicationId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
@@ -65,8 +61,6 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
</AddDomain>
|
</AddDomain>
|
||||||
|
|
||||||
<GenerateDomain applicationId={applicationId} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -77,7 +71,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
key={item.domainId}
|
key={item.domainId}
|
||||||
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
||||||
>
|
>
|
||||||
<Link target="_blank" href={`http://${item.host}`}>
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||||
|
>
|
||||||
<ExternalLink className="size-5" />
|
<ExternalLink className="size-5" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Password" {...field} />
|
<Input placeholder="Password" {...field} type="password" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -41,6 +41,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateApplication = ({ applicationId }: Props) => {
|
export const UpdateApplication = ({ applicationId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
api.application.update.useMutation();
|
api.application.update.useMutation();
|
||||||
@@ -79,6 +80,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
utils.application.one.invalidate({
|
utils.application.one.invalidate({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the application");
|
toast.error("Error to update the application");
|
||||||
@@ -87,7 +89,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
441
apps/dokploy/components/dashboard/compose/domains/add-domain.tsx
Normal file
441
apps/dokploy/components/dashboard/compose/domains/add-domain.tsx
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { domainCompose } from "@/server/db/validations/domain";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
|
type Domain = z.infer<typeof domainCompose>;
|
||||||
|
|
||||||
|
export type CacheType = "fetch" | "cache";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
domainId?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddDomainCompose = ({
|
||||||
|
composeId,
|
||||||
|
domainId = "",
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
|
{
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!domainId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: compose } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!composeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||||
|
api.domain.generateDomain.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
|
? api.domain.update.useMutation()
|
||||||
|
: api.domain.create.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<Domain>({
|
||||||
|
resolver: zodResolver(domainCompose),
|
||||||
|
});
|
||||||
|
|
||||||
|
const https = form.watch("https");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
...data,
|
||||||
|
/* Convert null to undefined */
|
||||||
|
path: data?.path || undefined,
|
||||||
|
port: data?.port || undefined,
|
||||||
|
serviceName: data?.serviceName || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainId) {
|
||||||
|
form.reset({});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data, isLoading]);
|
||||||
|
|
||||||
|
const dictionary = {
|
||||||
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
|
error: domainId
|
||||||
|
? "Error to update the domain"
|
||||||
|
: "Error to create the domain",
|
||||||
|
submit: domainId ? "Update" : "Create",
|
||||||
|
dialogDescription: domainId
|
||||||
|
? "In this section you can edit a domain"
|
||||||
|
: "In this section you can add domains",
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: Domain) => {
|
||||||
|
await mutateAsync({
|
||||||
|
domainId,
|
||||||
|
composeId,
|
||||||
|
domainType: "compose",
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.domain.byComposeId.invalidate({
|
||||||
|
composeId,
|
||||||
|
});
|
||||||
|
toast.success(dictionary.success);
|
||||||
|
if (domainId) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(dictionary.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
{children}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Domain</DialogTitle>
|
||||||
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock
|
||||||
|
type="warning"
|
||||||
|
className="[overflow-wrap:anywhere]"
|
||||||
|
>
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-row gap-4 w-full items-end">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load the
|
||||||
|
services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this
|
||||||
|
compose, it will read the services from the
|
||||||
|
last deployment/fetch from the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="host"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Host</FormLabel>
|
||||||
|
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="api.dokploy.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingGenerate}
|
||||||
|
onClick={() => {
|
||||||
|
generateDomain({
|
||||||
|
appName: compose?.appName || "",
|
||||||
|
})
|
||||||
|
.then((domain) => {
|
||||||
|
field.onChange(domain);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dices className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>Generate traefik.me domain</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="path"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"/"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Container Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={"3000"}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(Number.parseInt(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{https && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="certificateType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Certificate</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a certificate" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Letsencrypt (Default)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
form="hook-form"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{dictionary.submit}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { DeleteDomain } from "../../application/domains/delete-domain";
|
||||||
|
import { AddDomainCompose } from "./add-domain";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||||
|
const { data } = api.domain.byComposeId.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!composeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<CardTitle className="text-xl">Domains</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Domains are used to access to the application
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
|
{data && data?.length > 0 && (
|
||||||
|
<AddDomainCompose composeId={composeId}>
|
||||||
|
<Button>
|
||||||
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
|
</Button>
|
||||||
|
</AddDomainCompose>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<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
|
||||||
|
domain
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
|
<AddDomainCompose composeId={composeId}>
|
||||||
|
<Button>
|
||||||
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
|
</Button>
|
||||||
|
</AddDomainCompose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full flex-col gap-4">
|
||||||
|
{data?.map((item) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.domainId}
|
||||||
|
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
|
||||||
|
>
|
||||||
|
<Link target="_blank" href={`http://${item.host}`}>
|
||||||
|
<ExternalLink className="size-5" />
|
||||||
|
</Link>
|
||||||
|
<Button variant="outline" disabled>
|
||||||
|
{item.serviceName}
|
||||||
|
</Button>
|
||||||
|
<Input disabled value={item.host} />
|
||||||
|
<Button variant="outline" disabled>
|
||||||
|
{item.path}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" disabled>
|
||||||
|
{item.port}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" disabled>
|
||||||
|
{item.https ? "HTTPS" : "HTTP"}
|
||||||
|
</Button>
|
||||||
|
<div className="flex flex-row gap-1">
|
||||||
|
<AddDomainCompose
|
||||||
|
composeId={composeId}
|
||||||
|
domainId={item.domainId}
|
||||||
|
>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</AddDomainCompose>
|
||||||
|
<DeleteDomain domainId={item.domainId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -72,7 +72,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose config Updated");
|
toast.success("Compose config Updated");
|
||||||
refetch();
|
refetch();
|
||||||
await utils.compose.allServices.invalidate({
|
await utils.compose.getConvertedCompose.invalidate({
|
||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { GitBranch, LockIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ComposeFileEditor } from "../compose-file-editor";
|
import { ComposeFileEditor } from "../compose-file-editor";
|
||||||
|
import { ShowConvertedCompose } from "../show-converted-compose";
|
||||||
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
Select the source of your code
|
Select the source of your code
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
<div className="hidden space-y-1 text-sm font-normal md:flex flex-row items-center gap-2">
|
||||||
|
<ShowConvertedCompose composeId={composeId} />
|
||||||
<GitBranch className="size-6 text-muted-foreground" />
|
<GitBranch className="size-6 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Puzzle, RefreshCw } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const {
|
||||||
|
data: compose,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
|
refetch,
|
||||||
|
} = api.compose.getConvertedCompose.useQuery(
|
||||||
|
{ composeId },
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="max-lg:w-full" variant="outline">
|
||||||
|
<Puzzle className="h-4 w-4" />
|
||||||
|
Preview Compose
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Converted Compose</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Preview your docker-compose file with added domains. Note: At least
|
||||||
|
one domain must be specified for this conversion to take effect.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={() => {
|
||||||
|
mutateAsync({ composeId })
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Fetched source type");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error("Error to fetch source type", {
|
||||||
|
description: err.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh <RefreshCw className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<CodeEditor
|
||||||
|
value={compose || ""}
|
||||||
|
language="yaml"
|
||||||
|
readOnly
|
||||||
|
height="50rem"
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { SquarePen } from "lucide-react";
|
import { SquarePen } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -41,6 +41,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateCompose = ({ composeId }: Props) => {
|
export const UpdateCompose = ({ composeId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
api.compose.update.useMutation();
|
api.compose.update.useMutation();
|
||||||
@@ -79,6 +80,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
|||||||
utils.compose.one.invalidate({
|
utils.compose.one.invalidate({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Compose");
|
toast.error("Error to update the Compose");
|
||||||
@@ -87,7 +89,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -57,6 +57,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data, isLoading } = api.destination.all.useQuery();
|
const { data, isLoading } = api.destination.all.useQuery();
|
||||||
const { data: backup } = api.backup.one.useQuery(
|
const { data: backup } = api.backup.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -105,6 +106,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Backup Updated");
|
toast.success("Backup Updated");
|
||||||
refetch();
|
refetch();
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the backup");
|
toast.error("Error to update the backup");
|
||||||
@@ -112,7 +114,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`http:
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -41,6 +41,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateMongo = ({ mongoId }: Props) => {
|
export const UpdateMongo = ({ mongoId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
api.mongo.update.useMutation();
|
api.mongo.update.useMutation();
|
||||||
@@ -79,6 +80,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
|||||||
utils.mongo.one.invalidate({
|
utils.mongo.one.invalidate({
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update mongo database");
|
toast.error("Error to update mongo database");
|
||||||
@@ -87,7 +89,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -41,6 +41,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdatePostgres = ({ postgresId }: Props) => {
|
export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
api.postgres.update.useMutation();
|
api.postgres.update.useMutation();
|
||||||
@@ -79,6 +80,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
|||||||
utils.postgres.one.invalidate({
|
utils.postgres.one.invalidate({
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
});
|
});
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the postgres");
|
toast.error("Error to update the postgres");
|
||||||
@@ -87,7 +89,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
<SquarePen className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ interface Props {
|
|||||||
|
|
||||||
export const AddTemplate = ({ projectId }: Props) => {
|
export const AddTemplate = ({ projectId }: Props) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const { data } = api.compose.templates.useQuery();
|
const { data } = api.compose.templates.useQuery();
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
const { data: tags, isLoading: isLoadingTags } =
|
const { data: tags, isLoading: isLoadingTags } =
|
||||||
@@ -75,14 +76,14 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<PuzzleIcon className="size-4 text-muted-foreground" />
|
<PuzzleIcon className="size-4 text-muted-foreground" />
|
||||||
<span>Templates</span>
|
<span>Template</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
|
||||||
@@ -283,6 +284,7 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
utils.project.one.invalidate({
|
utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
|
setOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -42,6 +42,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateProject = ({ projectId }: Props) => {
|
export const UpdateProject = ({ projectId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError } = api.project.update.useMutation();
|
const { mutateAsync, error, isError } = api.project.update.useMutation();
|
||||||
const { data } = api.project.one.useQuery(
|
const { data } = api.project.one.useQuery(
|
||||||
@@ -77,6 +78,7 @@ export const UpdateProject = ({ projectId }: Props) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Project updated succesfully");
|
toast.success("Project updated succesfully");
|
||||||
utils.project.all.invalidate();
|
utils.project.all.invalidate();
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the project");
|
toast.error("Error to update the project");
|
||||||
@@ -85,7 +87,7 @@ export const UpdateProject = ({ projectId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ export const AddManager = () => {
|
|||||||
<div className="flex flex-col gap-2.5 text-sm">
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
<span>1. Go to your new server and run the following command</span>
|
<span>1. Go to your new server and run the following command</span>
|
||||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||||
curl https://get.docker.com | sh -s -- --version 24.0
|
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="self-center"
|
className="self-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copy("curl https://get.docker.com | sh -s -- --version 24.0");
|
copy(
|
||||||
|
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
||||||
|
);
|
||||||
toast.success("Copied to clipboard");
|
toast.success("Copied to clipboard");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -43,12 +45,12 @@ export const AddManager = () => {
|
|||||||
cluster
|
cluster
|
||||||
</span>
|
</span>
|
||||||
<span className="bg-muted rounded-lg p-2 flex">
|
<span className="bg-muted rounded-lg p-2 flex">
|
||||||
{data}
|
{data?.command}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="self-start"
|
className="self-start"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copy(data || "");
|
copy(data?.command || "");
|
||||||
toast.success("Copied to clipboard");
|
toast.success("Copied to clipboard");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ export const AddWorker = () => {
|
|||||||
<div className="flex flex-col gap-2.5 text-sm">
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
<span>1. Go to your new server and run the following command</span>
|
<span>1. Go to your new server and run the following command</span>
|
||||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||||
curl https://get.docker.com | sh -s -- --version 24.0
|
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="self-center"
|
className="self-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copy("curl https://get.docker.com | sh -s -- --version 24.0");
|
copy(
|
||||||
|
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
||||||
|
);
|
||||||
toast.success("Copied to clipboard");
|
toast.success("Copied to clipboard");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -42,12 +44,12 @@ export const AddWorker = () => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="bg-muted rounded-lg p-2 flex">
|
<span className="bg-muted rounded-lg p-2 flex">
|
||||||
{data}
|
{data?.command}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="self-start"
|
className="self-start"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copy(data || "");
|
copy(data?.command || "");
|
||||||
toast.success("Copied to clipboard");
|
toast.success("Copied to clipboard");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -43,6 +43,7 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateDestination = ({ destinationId }: Props) => {
|
export const UpdateDestination = ({ destinationId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data, refetch } = api.destination.one.useQuery(
|
const { data, refetch } = api.destination.one.useQuery(
|
||||||
{
|
{
|
||||||
destinationId,
|
destinationId,
|
||||||
@@ -93,13 +94,14 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
toast.success("Destination Updated");
|
toast.success("Destination Updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
await utils.destination.all.invalidate();
|
await utils.destination.all.invalidate();
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Destination");
|
toast.error("Error to update the Destination");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -9,36 +9,11 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { format } from "date-fns";
|
||||||
import { BadgeCheck } from "lucide-react";
|
import { BadgeCheck } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { RemoveGithubApp } from "./remove-github-app";
|
import { RemoveGithubApp } from "./remove-github-app";
|
||||||
export const generateName = () => {
|
|
||||||
const n1 = ["Blue", "Green", "Red", "Orange", "Violet", "Indigo", "Yellow"];
|
|
||||||
const n2 = [
|
|
||||||
"One",
|
|
||||||
"Two",
|
|
||||||
"Three",
|
|
||||||
"Four",
|
|
||||||
"Five",
|
|
||||||
"Six",
|
|
||||||
"Seven",
|
|
||||||
"Eight",
|
|
||||||
"Nine",
|
|
||||||
"Zero",
|
|
||||||
];
|
|
||||||
return `Dokploy-${n1[Math.round(Math.random() * (n1.length - 1))]}-${
|
|
||||||
n2[Math.round(Math.random() * (n2.length - 1))]
|
|
||||||
}`;
|
|
||||||
};
|
|
||||||
function slugify(text: string) {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[\s\^&*()+=!]+/g, "-")
|
|
||||||
.replace(/[\$.,*+~()'"!:@^&]+/g, "")
|
|
||||||
.replace(/-+/g, "-")
|
|
||||||
.replace(/^-+|-+$/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GithubSetup = () => {
|
export const GithubSetup = () => {
|
||||||
const [isOrganization, setIsOrganization] = useState(false);
|
const [isOrganization, setIsOrganization] = useState(false);
|
||||||
@@ -52,10 +27,9 @@ export const GithubSetup = () => {
|
|||||||
const manifest = JSON.stringify(
|
const manifest = JSON.stringify(
|
||||||
{
|
{
|
||||||
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
|
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
|
||||||
name: generateName(),
|
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||||
url: origin,
|
url: origin,
|
||||||
hook_attributes: {
|
hook_attributes: {
|
||||||
// JUST FOR TESTING
|
|
||||||
url: `${url}/api/deploy/github`,
|
url: `${url}/api/deploy/github`,
|
||||||
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
|
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
|
||||||
},
|
},
|
||||||
@@ -95,8 +69,8 @@ export const GithubSetup = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-4 flex-wrap">
|
<div className="flex items-end gap-4 flex-wrap">
|
||||||
<RemoveGithubApp />
|
<RemoveGithubApp />
|
||||||
{/* <Link
|
<Link
|
||||||
href={`https://github.com/settings/apps/${data?.githubAppName}`}
|
href={`${data?.githubAppName}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
className: "w-fit",
|
className: "w-fit",
|
||||||
@@ -104,7 +78,7 @@ export const GithubSetup = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span className="text-sm">Manage Github App</span>
|
<span className="text-sm">Manage Github App</span>
|
||||||
</Link> */}
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -119,9 +93,9 @@ export const GithubSetup = () => {
|
|||||||
|
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/apps/${slugify(
|
href={`${
|
||||||
data.githubAppName,
|
data.githubAppName
|
||||||
)}/installations/new?state=gh_setup:${data?.authId}`}
|
}/installations/new?state=gh_setup:${data?.authId}`}
|
||||||
className={buttonVariants({ className: "w-fit" })}
|
className={buttonVariants({ className: "w-fit" })}
|
||||||
>
|
>
|
||||||
Install Github App
|
Install Github App
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Mail, PenBoxIcon } from "lucide-react";
|
import { Mail, PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
|
import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
@@ -41,6 +41,7 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateNotification = ({ notificationId }: Props) => {
|
export const UpdateNotification = ({ notificationId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data, refetch } = api.notification.one.useQuery(
|
const { data, refetch } = api.notification.one.useQuery(
|
||||||
{
|
{
|
||||||
notificationId,
|
notificationId,
|
||||||
@@ -207,6 +208,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
toast.success("Notification Updated");
|
toast.success("Notification Updated");
|
||||||
await utils.notification.all.invalidate();
|
await utils.notification.all.invalidate();
|
||||||
refetch();
|
refetch();
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update a notification");
|
toast.error("Error to update a notification");
|
||||||
@@ -214,7 +216,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const ShowDestinations = () => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">SSH Keys</CardTitle>
|
<CardTitle className="text-xl">SSH Keys</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Use SSH to beeing able cloning from private repositories.
|
Use SSH to be able to clone from private repositories.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 pt-4">
|
<CardContent className="space-y-2 pt-4">
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const addPermissions = z.object({
|
|||||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||||
canAccessToDocker: z.boolean().optional().default(false),
|
canAccessToDocker: z.boolean().optional().default(false),
|
||||||
canAccessToAPI: z.boolean().optional().default(false),
|
canAccessToAPI: z.boolean().optional().default(false),
|
||||||
|
canAccessToSSHKeys: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddPermissions = z.infer<typeof addPermissions>;
|
type AddPermissions = z.infer<typeof addPermissions>;
|
||||||
@@ -82,6 +83,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
canAccessToAPI: data.canAccessToAPI,
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
|
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||||
@@ -98,6 +100,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
accesedServices: data.accesedServices || [],
|
accesedServices: data.accesedServices || [],
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
canAccessToAPI: data.canAccessToAPI,
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
|
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Permissions updated");
|
toast.success("Permissions updated");
|
||||||
@@ -270,6 +273,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="canAccessToSSHKeys"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Access to SSH Keys</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Allow to users to access to the SSH Keys section
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="accesedProjects"
|
name="accesedProjects"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
|
||||||
|
import { EditTraefikEnv } from "./web-server/edit-traefik-env";
|
||||||
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
|
||||||
import { ShowModalLogs } from "./web-server/show-modal-logs";
|
import { ShowModalLogs } from "./web-server/show-modal-logs";
|
||||||
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
|
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
|
||||||
@@ -67,6 +68,9 @@ export const WebServer = () => {
|
|||||||
const { mutateAsync: updateDockerCleanup } =
|
const { mutateAsync: updateDockerCleanup } =
|
||||||
api.settings.updateDockerCleanup.useMutation();
|
api.settings.updateDockerCleanup.useMutation();
|
||||||
|
|
||||||
|
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
|
||||||
|
api.settings.haveTraefikDashboardPortEnabled.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="rounded-lg w-full bg-transparent">
|
<Card className="rounded-lg w-full bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -167,37 +171,38 @@ export const WebServer = () => {
|
|||||||
<span>View Traefik config</span>
|
<span>View Traefik config</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</ShowMainTraefikConfig>
|
</ShowMainTraefikConfig>
|
||||||
|
<EditTraefikEnv>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
>
|
||||||
|
<span>Modify Env</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</EditTraefikEnv>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await toggleDashboard({
|
await toggleDashboard({
|
||||||
enableDashboard: true,
|
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Dashboard Enabled");
|
toast.success(
|
||||||
|
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||||
|
);
|
||||||
|
refetchDashboard();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to enable Dashboard");
|
toast.error(
|
||||||
|
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
>
|
>
|
||||||
<span>Enable Dashboard</span>
|
<span>
|
||||||
</DropdownMenuItem>
|
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
|
||||||
<DropdownMenuItem
|
Dashboard
|
||||||
onClick={async () => {
|
</span>
|
||||||
await toggleDashboard({
|
|
||||||
enableDashboard: false,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Dashboard Disabled");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to disable Dashboard");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="w-full cursor-pointer space-x-3"
|
|
||||||
>
|
|
||||||
<span>Disable Dashboard</span>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DockerTerminalModal appName="dokploy-traefik">
|
<DockerTerminalModal appName="dokploy-traefik">
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
env: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditTraefikEnv = ({ children }: Props) => {
|
||||||
|
const [canEdit, setCanEdit] = useState(true);
|
||||||
|
|
||||||
|
const { data } = api.settings.readTraefikEnv.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.settings.writeTraefikEnv.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
env: data || "",
|
||||||
|
},
|
||||||
|
disabled: canEdit,
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
env: data || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: Schema) => {
|
||||||
|
await mutateAsync(data.env)
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Traefik Env Updated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update the traefik env");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Traefik Env</DialogTitle>
|
||||||
|
<DialogDescription>Update the traefik env</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-update-server-traefik-config"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="w-full space-y-4 relative overflow-auto"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="env"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>Env</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
language="properties"
|
||||||
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
|
placeholder={`TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=test@localhost.com
|
||||||
|
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_STORAGE=/etc/dokploy/traefik/dynamic/acme.json
|
||||||
|
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE=true
|
||||||
|
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_PRETTY=true
|
||||||
|
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_ENTRYPOINT=web
|
||||||
|
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_DNS_CHALLENGE=true
|
||||||
|
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_HTTP_CHALLENGE_DNS_PROVIDER=cloudflare
|
||||||
|
`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
<div className="flex justify-end absolute z-50 right-6 top-0">
|
||||||
|
<Button
|
||||||
|
className="shadow-sm"
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
setCanEdit(!canEdit);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{canEdit ? "Unlock" : "Lock"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={canEdit || isLoading}
|
||||||
|
form="hook-form-update-server-traefik-config"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -106,6 +106,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
|||||||
<FormLabel>Traefik config</FormLabel>
|
<FormLabel>Traefik config</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`providers:
|
placeholder={`providers:
|
||||||
docker:
|
docker:
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
|||||||
<FormLabel>Traefik config</FormLabel>
|
<FormLabel>Traefik config</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`http:
|
placeholder={`http:
|
||||||
routers:
|
routers:
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(user?.canAccessToSSHKeys
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "SSH Keys",
|
||||||
|
label: "",
|
||||||
|
icon: KeyRound,
|
||||||
|
href: "/dashboard/settings/ssh-keys",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { json } from "@codemirror/lang-json";
|
|||||||
import { yaml } from "@codemirror/lang-yaml";
|
import { yaml } from "@codemirror/lang-yaml";
|
||||||
import { StreamLanguage } from "@codemirror/language";
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||||
|
import { EditorView } from "@codemirror/view";
|
||||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
@@ -10,6 +11,7 @@ interface Props extends ReactCodeMirrorProps {
|
|||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
language?: "yaml" | "json" | "properties";
|
language?: "yaml" | "json" | "properties";
|
||||||
|
lineWrapping?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeEditor = ({
|
export const CodeEditor = ({
|
||||||
@@ -36,6 +38,7 @@ export const CodeEditor = ({
|
|||||||
: language === "json"
|
: language === "json"
|
||||||
? json()
|
? json()
|
||||||
: StreamLanguage.define(properties),
|
: StreamLanguage.define(properties),
|
||||||
|
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||||
]}
|
]}
|
||||||
{...props}
|
{...props}
|
||||||
editable={!props.disabled}
|
editable={!props.disabled}
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0027_red_lady_bullseye.sql
Normal file
1
apps/dokploy/drizzle/0027_red_lady_bullseye.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "publishDirectory" text;
|
||||||
1
apps/dokploy/drizzle/0028_jittery_eternity.sql
Normal file
1
apps/dokploy/drizzle/0028_jittery_eternity.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "buildType" ADD VALUE 'static';
|
||||||
1
apps/dokploy/drizzle/0029_colossal_zodiak.sql
Normal file
1
apps/dokploy/drizzle/0029_colossal_zodiak.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "dockerContextPath" text;
|
||||||
1
apps/dokploy/drizzle/0030_little_kabuki.sql
Normal file
1
apps/dokploy/drizzle/0030_little_kabuki.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ADD COLUMN "canAccessToSSHKeys" boolean DEFAULT false NOT NULL;
|
||||||
15
apps/dokploy/drizzle/0031_steep_vulture.sql
Normal file
15
apps/dokploy/drizzle/0031_steep_vulture.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE "public"."domainType" AS ENUM('compose', 'application');
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "domain" ALTER COLUMN "applicationId" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "domain" ADD COLUMN "serviceName" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "domain" ADD COLUMN "domainType" "domainType" DEFAULT 'application';--> statement-breakpoint
|
||||||
|
ALTER TABLE "domain" ADD COLUMN "composeId" text;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "domain" ADD CONSTRAINT "domain_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
1
apps/dokploy/drizzle/0032_flashy_shadow_king.sql
Normal file
1
apps/dokploy/drizzle/0032_flashy_shadow_king.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "domain" ALTER COLUMN "port" SET DEFAULT 3000;
|
||||||
3016
apps/dokploy/drizzle/meta/0027_snapshot.json
Normal file
3016
apps/dokploy/drizzle/meta/0027_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3017
apps/dokploy/drizzle/meta/0028_snapshot.json
Normal file
3017
apps/dokploy/drizzle/meta/0028_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3023
apps/dokploy/drizzle/meta/0029_snapshot.json
Normal file
3023
apps/dokploy/drizzle/meta/0029_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3030
apps/dokploy/drizzle/meta/0030_snapshot.json
Normal file
3030
apps/dokploy/drizzle/meta/0030_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3071
apps/dokploy/drizzle/meta/0031_snapshot.json
Normal file
3071
apps/dokploy/drizzle/meta/0031_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3071
apps/dokploy/drizzle/meta/0032_snapshot.json
Normal file
3071
apps/dokploy/drizzle/meta/0032_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,48 @@
|
|||||||
"when": 1721979220929,
|
"when": 1721979220929,
|
||||||
"tag": "0026_known_dormammu",
|
"tag": "0026_known_dormammu",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 27,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1722445099203,
|
||||||
|
"tag": "0027_red_lady_bullseye",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 28,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1722503439951,
|
||||||
|
"tag": "0028_jittery_eternity",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 29,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1722578386823,
|
||||||
|
"tag": "0029_colossal_zodiak",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 30,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1723608499147,
|
||||||
|
"tag": "0030_little_kabuki",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 31,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1723701656243,
|
||||||
|
"tag": "0031_steep_vulture",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 32,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1723705257806,
|
||||||
|
"tag": "0032_flashy_shadow_king",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.5.1",
|
"version": "v0.7.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
"@codemirror/legacy-modes": "6.4.0",
|
"@codemirror/legacy-modes": "6.4.0",
|
||||||
|
"@codemirror/view": "6.29.0",
|
||||||
"@dokploy/trpc-openapi": "0.0.4",
|
"@dokploy/trpc-openapi": "0.0.4",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default async function handler(
|
|||||||
.update(admins)
|
.update(admins)
|
||||||
.set({
|
.set({
|
||||||
githubAppId: data.id,
|
githubAppId: data.id,
|
||||||
githubAppName: data.name,
|
githubAppName: data.html_url,
|
||||||
githubClientId: data.client_id,
|
githubClientId: data.client_id,
|
||||||
githubClientSecret: data.client_secret,
|
githubClientSecret: data.client_secret,
|
||||||
githubWebhookSecret: data.webhook_secret,
|
githubWebhookSecret: data.webhook_secret,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-c
|
|||||||
import { ShowVolumesCompose } from "@/components/dashboard/compose/advanced/show-volumes";
|
import { ShowVolumesCompose } from "@/components/dashboard/compose/advanced/show-volumes";
|
||||||
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
|
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
|
||||||
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
|
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
|
||||||
|
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
|
||||||
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
|
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
|
||||||
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
||||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||||
@@ -34,6 +35,7 @@ type TabState =
|
|||||||
| "settings"
|
| "settings"
|
||||||
| "advanced"
|
| "advanced"
|
||||||
| "deployments"
|
| "deployments"
|
||||||
|
| "domains"
|
||||||
| "monitoring";
|
| "monitoring";
|
||||||
|
|
||||||
const Service = (
|
const Service = (
|
||||||
@@ -117,12 +119,13 @@ const Service = (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
|
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
@@ -168,6 +171,12 @@ const Service = (
|
|||||||
<ShowDeploymentsCompose composeId={composeId} />
|
<ShowDeploymentsCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="domains">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowDomainsCompose composeId={composeId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<AddCommandCompose composeId={composeId} />
|
<AddCommandCompose composeId={composeId} />
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
|
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -26,7 +29,7 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -34,8 +37,45 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const { req, res, resolvedUrl } = ctx;
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
try {
|
||||||
props: {},
|
await helpers.project.all.prefetch();
|
||||||
};
|
const auth = await helpers.auth.get.fetch();
|
||||||
|
|
||||||
|
if (auth.rol === "user") {
|
||||||
|
const user = await helpers.user.byAuthId.fetch({
|
||||||
|
authId: auth.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user.canAccessToSSHKeys) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
apps/dokploy/public/templates/aptabase.svg
Normal file
5
apps/dokploy/public/templates/aptabase.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg class="w-12 text-primary" viewBox="0 0 1000 760" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="#1a61ff"
|
||||||
|
d="M626.7 177.36c-55.8-98.4-197.59-98.4-253.39 0L112.97 636.44H500c0-51.67 41.88-93.55 93.55-93.55h22.09l57.82 93.55h213.57L626.69 177.37Zm-11.06 365.52-70.21-123.82c-20.01-35.28-70.84-35.28-90.85 0l-70.21 123.82H273.58l181.01-319.19c20.01-35.28 70.84-35.28 90.85 0l181.01 319.19H615.66Z"
|
||||||
|
style="--darkreader-inline-fill:currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 465 B |
BIN
apps/dokploy/public/templates/soketi.png
Normal file
BIN
apps/dokploy/public/templates/soketi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
15
apps/dokploy/public/templates/supabase.svg
Normal file
15
apps/dokploy/public/templates/supabase.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0625L99.1935 40.0625C107.384 40.0625 111.952 49.5226 106.859 55.9372L63.7076 110.284Z" fill="url(#paint0_linear)"/>
|
||||||
|
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0625L99.1935 40.0625C107.384 40.0625 111.952 49.5226 106.859 55.9372L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
|
||||||
|
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear" x1="53.9738" y1="54.9738" x2="94.1635" y2="71.8293" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#249361"/>
|
||||||
|
<stop offset="1" stop-color="#3ECF8E"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear" x1="36.1558" y1="30.5779" x2="54.4844" y2="65.0804" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop/>
|
||||||
|
<stop offset="1" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
apps/dokploy/public/templates/teable.png
Normal file
BIN
apps/dokploy/public/templates/teable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 757 B |
13
apps/dokploy/public/templates/typebot.svg
Normal file
13
apps/dokploy/public/templates/typebot.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="800" height="800" rx="80" fill="#0042DA" style="--darkreader-inline-fill:#0035ae" />
|
||||||
|
<rect x="650" y="293" width="85.47" height="384.617" rx="20" transform="rotate(90 650 293)" fill="#FF8E20"
|
||||||
|
style="--darkreader-inline-fill:#ff9630" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M192.735 378.47c23.602 0 42.735-19.133 42.735-42.735S216.337 293 192.735 293 150 312.133 150 335.735s19.133 42.735 42.735 42.735Z"
|
||||||
|
fill="#FF8E20" style="--darkreader-inline-fill:#ff9630" />
|
||||||
|
<rect x="150" y="506.677" width="85.47" height="384.617" rx="20" transform="rotate(-90 150 506.677)" fill="#fff"
|
||||||
|
style="--darkreader-inline-fill:#e8e6e3" />
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M607.265 421.206c-23.602 0-42.735 19.134-42.735 42.736 0 23.602 19.133 42.735 42.735 42.735S650 487.544 650 463.942s-19.133-42.736-42.735-42.736Z"
|
||||||
|
fill="#fff" style="--darkreader-inline-fill:#e8e6e3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1000 B |
BIN
apps/dokploy/public/templates/zipline.png
Normal file
BIN
apps/dokploy/public/templates/zipline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -191,6 +191,8 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
buildType: input.buildType,
|
buildType: input.buildType,
|
||||||
dockerfile: input.dockerfile,
|
dockerfile: input.dockerfile,
|
||||||
|
publishDirectory: input.publishDirectory,
|
||||||
|
dockerContextPath: input.dockerContextPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
|
import { db } from "../../db";
|
||||||
import {
|
import {
|
||||||
createAdmin,
|
createAdmin,
|
||||||
createUser,
|
createUser,
|
||||||
@@ -33,6 +34,14 @@ export const authRouter = createTRPCRouter({
|
|||||||
.input(apiCreateAdmin)
|
.input(apiCreateAdmin)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
|
const admin = await db.query.admins.findFirst({});
|
||||||
|
|
||||||
|
if (admin) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Admin already exists",
|
||||||
|
});
|
||||||
|
}
|
||||||
const newAdmin = await createAdmin(input);
|
const newAdmin = await createAdmin(input);
|
||||||
const session = await lucia.createSession(newAdmin.id || "", {});
|
const session = await lucia.createSession(newAdmin.id || "", {});
|
||||||
ctx.res.appendHeader(
|
ctx.res.appendHeader(
|
||||||
|
|||||||
@@ -35,14 +35,23 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
addWorker: protectedProcedure.query(async ({ input }) => {
|
addWorker: protectedProcedure.query(async ({ input }) => {
|
||||||
const result = await docker.swarmInspect();
|
const result = await docker.swarmInspect();
|
||||||
return `docker swarm join --token ${
|
const docker_version = await docker.version();
|
||||||
result.JoinTokens.Worker
|
|
||||||
} ${await getPublicIpWithFallback()}:2377`;
|
return {
|
||||||
|
command: `docker swarm join --token ${
|
||||||
|
result.JoinTokens.Worker
|
||||||
|
} ${await getPublicIpWithFallback()}:2377`,
|
||||||
|
version: docker_version.Version,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
addManager: protectedProcedure.query(async ({ input }) => {
|
addManager: protectedProcedure.query(async ({ input }) => {
|
||||||
const result = await docker.swarmInspect();
|
const result = await docker.swarmInspect();
|
||||||
return `docker swarm join --token ${
|
const docker_version = await docker.version();
|
||||||
result.JoinTokens.Manager
|
return {
|
||||||
} ${await getPublicIpWithFallback()}:2377`;
|
command: `docker swarm join --token ${
|
||||||
|
result.JoinTokens.Manager
|
||||||
|
} ${await getPublicIpWithFallback()}:2377`,
|
||||||
|
version: docker_version.Version,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { db } from "@/server/db";
|
|||||||
import {
|
import {
|
||||||
apiCreateCompose,
|
apiCreateCompose,
|
||||||
apiCreateComposeByTemplate,
|
apiCreateComposeByTemplate,
|
||||||
|
apiFetchServices,
|
||||||
apiFindCompose,
|
apiFindCompose,
|
||||||
apiRandomizeCompose,
|
apiRandomizeCompose,
|
||||||
apiUpdateCompose,
|
apiUpdateCompose,
|
||||||
@@ -15,16 +16,18 @@ import {
|
|||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { createCommand } from "@/server/utils/builders/compose";
|
import { createCommand } from "@/server/utils/builders/compose";
|
||||||
import { randomizeComposeFile } from "@/server/utils/docker/compose";
|
import { randomizeComposeFile } from "@/server/utils/docker/compose";
|
||||||
|
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
|
||||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
import {
|
import {
|
||||||
generatePassword,
|
generatePassword,
|
||||||
loadTemplateModule,
|
loadTemplateModule,
|
||||||
readComposeFile,
|
readTemplateComposeFile,
|
||||||
} from "@/templates/utils";
|
} from "@/templates/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { dump } from "js-yaml";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { findAdmin } from "../services/admin";
|
import { findAdmin } from "../services/admin";
|
||||||
@@ -38,6 +41,7 @@ import {
|
|||||||
updateCompose,
|
updateCompose,
|
||||||
} from "../services/compose";
|
} from "../services/compose";
|
||||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||||
|
import { createDomain, findDomainsByComposeId } from "../services/domain";
|
||||||
import { createMount } from "../services/mount";
|
import { createMount } from "../services/mount";
|
||||||
import { findProjectById } from "../services/project";
|
import { findProjectById } from "../services/project";
|
||||||
import { addNewService, checkServiceAccess } from "../services/user";
|
import { addNewService, checkServiceAccess } from "../services/user";
|
||||||
@@ -113,10 +117,25 @@ export const composeRouter = createTRPCRouter({
|
|||||||
await cleanQueuesByCompose(input.composeId);
|
await cleanQueuesByCompose(input.composeId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
allServices: protectedProcedure
|
loadServices: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFetchServices)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await loadServices(input.composeId);
|
return await loadServices(input.composeId, input.type);
|
||||||
|
}),
|
||||||
|
fetchSourceType: protectedProcedure
|
||||||
|
.input(apiFindCompose)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
|
await cloneCompose(compose);
|
||||||
|
return compose.sourceType;
|
||||||
|
} catch (err) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to fetch source type",
|
||||||
|
cause: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
randomizeCompose: protectedProcedure
|
randomizeCompose: protectedProcedure
|
||||||
@@ -124,6 +143,17 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return await randomizeComposeFile(input.composeId, input.prefix);
|
return await randomizeComposeFile(input.composeId, input.prefix);
|
||||||
}),
|
}),
|
||||||
|
getConvertedCompose: protectedProcedure
|
||||||
|
.input(apiFindCompose)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
|
const domains = await findDomainsByComposeId(input.composeId);
|
||||||
|
|
||||||
|
const composeFile = await addDomainToCompose(compose, domains);
|
||||||
|
return dump(composeFile, {
|
||||||
|
lineWidth: 1000,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
@@ -189,7 +219,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
||||||
}
|
}
|
||||||
const composeFile = await readComposeFile(input.id);
|
const composeFile = await readTemplateComposeFile(input.id);
|
||||||
|
|
||||||
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
const generate = await loadTemplateModule(input.id as TemplatesKeys);
|
||||||
|
|
||||||
@@ -206,7 +236,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const { envs, mounts } = generate({
|
const { envs, mounts, domains } = generate({
|
||||||
serverIp: admin.serverIp,
|
serverIp: admin.serverIp,
|
||||||
projectName: projectName,
|
projectName: projectName,
|
||||||
});
|
});
|
||||||
@@ -214,7 +244,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const compose = await createComposeByTemplate({
|
const compose = await createComposeByTemplate({
|
||||||
...input,
|
...input,
|
||||||
composeFile: composeFile,
|
composeFile: composeFile,
|
||||||
env: envs.join("\n"),
|
env: envs?.join("\n"),
|
||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
appName: `${projectName}-${generatePassword(6)}`,
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
@@ -227,7 +257,8 @@ export const composeRouter = createTRPCRouter({
|
|||||||
if (mounts && mounts?.length > 0) {
|
if (mounts && mounts?.length > 0) {
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
await createMount({
|
await createMount({
|
||||||
mountPath: mount.mountPath,
|
filePath: mount.filePath,
|
||||||
|
mountPath: "",
|
||||||
content: mount.content,
|
content: mount.content,
|
||||||
serviceId: compose.composeId,
|
serviceId: compose.composeId,
|
||||||
serviceType: "compose",
|
serviceType: "compose",
|
||||||
@@ -236,6 +267,17 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (domains && domains?.length > 0) {
|
||||||
|
for (const domain of domains) {
|
||||||
|
await createDomain({
|
||||||
|
...domain,
|
||||||
|
domainType: "compose",
|
||||||
|
certificateType: "none",
|
||||||
|
composeId: compose.composeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
apiCreateDomain,
|
apiCreateDomain,
|
||||||
|
apiCreateTraefikMeDomain,
|
||||||
|
apiFindCompose,
|
||||||
apiFindDomain,
|
apiFindDomain,
|
||||||
apiFindDomainByApplication,
|
apiFindDomainByApplication,
|
||||||
|
apiFindDomainByCompose,
|
||||||
|
apiFindOneApplication,
|
||||||
apiUpdateDomain,
|
apiUpdateDomain,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
|
import { manageDomain, removeDomain } from "@/server/utils/traefik/domain";
|
||||||
@@ -12,8 +16,8 @@ import {
|
|||||||
createDomain,
|
createDomain,
|
||||||
findDomainById,
|
findDomainById,
|
||||||
findDomainsByApplicationId,
|
findDomainsByApplicationId,
|
||||||
generateDomain,
|
findDomainsByComposeId,
|
||||||
generateWildcard,
|
generateTraefikMeDomain,
|
||||||
removeDomainById,
|
removeDomainById,
|
||||||
updateDomainById,
|
updateDomainById,
|
||||||
} from "../services/domain";
|
} from "../services/domain";
|
||||||
@@ -33,27 +37,30 @@ export const domainRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
byApplicationId: protectedProcedure
|
byApplicationId: protectedProcedure
|
||||||
.input(apiFindDomainByApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await findDomainsByApplicationId(input.applicationId);
|
return await findDomainsByApplicationId(input.applicationId);
|
||||||
}),
|
}),
|
||||||
|
byComposeId: protectedProcedure
|
||||||
|
.input(apiFindCompose)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await findDomainsByComposeId(input.composeId);
|
||||||
|
}),
|
||||||
generateDomain: protectedProcedure
|
generateDomain: protectedProcedure
|
||||||
.input(apiFindDomainByApplication)
|
.input(apiCreateTraefikMeDomain)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return generateDomain(input);
|
return generateTraefikMeDomain(input.appName);
|
||||||
}),
|
|
||||||
generateWildcard: protectedProcedure
|
|
||||||
.input(apiFindDomainByApplication)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return generateWildcard(input);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateDomain)
|
.input(apiUpdateDomain)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const result = await updateDomainById(input.domainId, input);
|
const result = await updateDomainById(input.domainId, input);
|
||||||
const domain = await findDomainById(input.domainId);
|
const domain = await findDomainById(input.domainId);
|
||||||
const application = await findApplicationById(domain.applicationId);
|
if (domain.applicationId) {
|
||||||
await manageDomain(application, domain);
|
const application = await findApplicationById(domain.applicationId);
|
||||||
|
await manageDomain(application, domain);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure.input(apiFindDomain).query(async ({ input }) => {
|
one: protectedProcedure.input(apiFindDomain).query(async ({ input }) => {
|
||||||
@@ -64,7 +71,9 @@ export const domainRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const domain = await findDomainById(input.domainId);
|
const domain = await findDomainById(input.domainId);
|
||||||
const result = await removeDomainById(input.domainId);
|
const result = await removeDomainById(input.domainId);
|
||||||
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
|
if (domain.application) {
|
||||||
|
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ import {
|
|||||||
cleanUpSystemPrune,
|
cleanUpSystemPrune,
|
||||||
cleanUpUnusedImages,
|
cleanUpUnusedImages,
|
||||||
cleanUpUnusedVolumes,
|
cleanUpUnusedVolumes,
|
||||||
|
prepareEnvironmentVariables,
|
||||||
startService,
|
startService,
|
||||||
stopService,
|
stopService,
|
||||||
} from "@/server/utils/docker/utils";
|
} from "@/server/utils/docker/utils";
|
||||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { sendDockerCleanupNotifications } from "@/server/utils/notifications/docker-cleanup";
|
import { sendDockerCleanupNotifications } from "@/server/utils/notifications/docker-cleanup";
|
||||||
|
import { execAsync } from "@/server/utils/process/execAsync";
|
||||||
import { spawnAsync } from "@/server/utils/process/spawnAsync";
|
import { spawnAsync } from "@/server/utils/process/spawnAsync";
|
||||||
import {
|
import {
|
||||||
readConfig,
|
readConfig,
|
||||||
@@ -36,6 +38,7 @@ import {
|
|||||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||||
|
import { z } from "zod";
|
||||||
import { appRouter } from "../root";
|
import { appRouter } from "../root";
|
||||||
import { findAdmin, updateAdmin } from "../services/admin";
|
import { findAdmin, updateAdmin } from "../services/admin";
|
||||||
import {
|
import {
|
||||||
@@ -49,14 +52,10 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
|||||||
|
|
||||||
export const settingsRouter = createTRPCRouter({
|
export const settingsRouter = createTRPCRouter({
|
||||||
reloadServer: adminProcedure.mutation(async () => {
|
reloadServer: adminProcedure.mutation(async () => {
|
||||||
await spawnAsync("docker", [
|
const { stdout } = await execAsync(
|
||||||
"service",
|
"docker service inspect dokploy --format '{{.ID}}'",
|
||||||
"update",
|
);
|
||||||
"--force",
|
await execAsync(`docker service update --force ${stdout.trim()}`);
|
||||||
"--image",
|
|
||||||
getDokployImage(),
|
|
||||||
"dokploy",
|
|
||||||
]);
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
reloadTraefik: adminProcedure.mutation(async () => {
|
reloadTraefik: adminProcedure.mutation(async () => {
|
||||||
@@ -72,7 +71,9 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
toggleDashboard: adminProcedure
|
toggleDashboard: adminProcedure
|
||||||
.input(apiEnableDashboard)
|
.input(apiEnableDashboard)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await initializeTraefik(input.enableDashboard);
|
await initializeTraefik({
|
||||||
|
enableDashboard: input.enableDashboard,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -312,4 +313,37 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
return openApiDocument;
|
return openApiDocument;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
readTraefikEnv: adminProcedure.query(async () => {
|
||||||
|
const { stdout } = await execAsync(
|
||||||
|
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik",
|
||||||
|
);
|
||||||
|
|
||||||
|
return stdout.trim();
|
||||||
|
}),
|
||||||
|
|
||||||
|
writeTraefikEnv: adminProcedure
|
||||||
|
.input(z.string())
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const envs = prepareEnvironmentVariables(input);
|
||||||
|
await initializeTraefik({
|
||||||
|
env: envs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
haveTraefikDashboardPortEnabled: adminProcedure.query(async () => {
|
||||||
|
const { stdout } = await execAsync(
|
||||||
|
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsed: any[] = JSON.parse(stdout.trim());
|
||||||
|
|
||||||
|
for (const port of parsed) {
|
||||||
|
if (port.PublishedPort === 8080) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,21 +34,23 @@ export const sshRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure.input(apiRemoveSshKey).mutation(async ({ input }) => {
|
remove: protectedProcedure
|
||||||
try {
|
.input(apiRemoveSshKey)
|
||||||
return await removeSSHKeyById(input.sshKeyId);
|
.mutation(async ({ input }) => {
|
||||||
} catch (error) {
|
try {
|
||||||
throw new TRPCError({
|
return await removeSSHKeyById(input.sshKeyId);
|
||||||
code: "BAD_REQUEST",
|
} catch (error) {
|
||||||
message: "Error to delete this ssh key",
|
throw new TRPCError({
|
||||||
});
|
code: "BAD_REQUEST",
|
||||||
}
|
message: "Error to delete this ssh key",
|
||||||
}),
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
one: protectedProcedure.input(apiFindOneSshKey).query(async ({ input }) => {
|
one: protectedProcedure.input(apiFindOneSshKey).query(async ({ input }) => {
|
||||||
const sshKey = await findSSHKeyById(input.sshKeyId);
|
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||||
return sshKey;
|
return sshKey;
|
||||||
}),
|
}),
|
||||||
all: adminProcedure.query(async () => {
|
all: protectedProcedure.query(async () => {
|
||||||
return await db.query.sshKeys.findMany({});
|
return await db.query.sshKeys.findMany({});
|
||||||
}),
|
}),
|
||||||
generate: protectedProcedure
|
generate: protectedProcedure
|
||||||
@@ -56,15 +58,17 @@ export const sshRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return await generateSSHKey(input.type);
|
return await generateSSHKey(input.type);
|
||||||
}),
|
}),
|
||||||
update: adminProcedure.input(apiUpdateSshKey).mutation(async ({ input }) => {
|
update: protectedProcedure
|
||||||
try {
|
.input(apiUpdateSshKey)
|
||||||
return await updateSSHKeyById(input);
|
.mutation(async ({ input }) => {
|
||||||
} catch (error) {
|
try {
|
||||||
throw new TRPCError({
|
return await updateSSHKeyById(input);
|
||||||
code: "BAD_REQUEST",
|
} catch (error) {
|
||||||
message: "Error to update this ssh key",
|
throw new TRPCError({
|
||||||
cause: error,
|
code: "BAD_REQUEST",
|
||||||
});
|
message: "Error to update this ssh key",
|
||||||
}
|
cause: error,
|
||||||
}),
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { db } from "@/server/db";
|
|||||||
import { type apiCreateCompose, compose } from "@/server/db/schema";
|
import { type apiCreateCompose, compose } from "@/server/db/schema";
|
||||||
import { generateAppName } from "@/server/db/schema/utils";
|
import { generateAppName } from "@/server/db/schema/utils";
|
||||||
import { buildCompose } from "@/server/utils/builders/compose";
|
import { buildCompose } from "@/server/utils/builders/compose";
|
||||||
|
import { cloneCompose, loadDockerCompose } from "@/server/utils/docker/domain";
|
||||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||||
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
|
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
|
||||||
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
|
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
|
||||||
@@ -14,7 +15,6 @@ import { createComposeFile } from "@/server/utils/providers/raw";
|
|||||||
import { generatePassword } from "@/templates/utils";
|
import { generatePassword } from "@/templates/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { findAdmin, getDokployUrl } from "./admin";
|
import { findAdmin, getDokployUrl } from "./admin";
|
||||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
||||||
import { validUniqueServerAppName } from "./project";
|
import { validUniqueServerAppName } from "./project";
|
||||||
@@ -91,6 +91,7 @@ export const findComposeById = async (composeId: string) => {
|
|||||||
project: true,
|
project: true,
|
||||||
deployments: true,
|
deployments: true,
|
||||||
mounts: true,
|
mounts: true,
|
||||||
|
domains: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -102,20 +103,27 @@ export const findComposeById = async (composeId: string) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadServices = async (composeId: string) => {
|
export const loadServices = async (
|
||||||
|
composeId: string,
|
||||||
|
type: "fetch" | "cache" = "fetch",
|
||||||
|
) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
|
|
||||||
// use js-yaml to parse the docker compose file and then extact the services
|
if (type === "fetch") {
|
||||||
const composeFile = compose.composeFile;
|
await cloneCompose(compose);
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
}
|
||||||
|
|
||||||
|
const composeData = await loadDockerCompose(compose);
|
||||||
if (!composeData?.services) {
|
if (!composeData?.services) {
|
||||||
return ["All Services"];
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Services not found",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const services = Object.keys(composeData.services);
|
const services = Object.keys(composeData.services);
|
||||||
|
|
||||||
return [...services, "All Services"];
|
return [...services];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCompose = async (
|
export const updateCompose = async (
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ export type Domain = typeof domains.$inferSelect;
|
|||||||
|
|
||||||
export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
|
||||||
|
|
||||||
const domain = await tx
|
const domain = await tx
|
||||||
.insert(domains)
|
.insert(domains)
|
||||||
.values({
|
.values({
|
||||||
@@ -32,52 +30,19 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await manageDomain(application, domain);
|
if (domain.applicationId) {
|
||||||
|
const application = await findApplicationById(domain.applicationId);
|
||||||
|
await manageDomain(application, domain);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateDomain = async (
|
export const generateTraefikMeDomain = async (appName: string) => {
|
||||||
input: typeof apiFindDomainByApplication._type,
|
|
||||||
) => {
|
|
||||||
const application = await findApplicationById(input.applicationId);
|
|
||||||
const admin = await findAdmin();
|
const admin = await findAdmin();
|
||||||
const domain = await createDomain({
|
return generateRandomDomain({
|
||||||
applicationId: application.applicationId,
|
serverIp: admin.serverIp || "",
|
||||||
host: generateRandomDomain({
|
projectName: appName,
|
||||||
serverIp: admin.serverIp || "",
|
|
||||||
projectName: application.appName,
|
|
||||||
}),
|
|
||||||
port: 3000,
|
|
||||||
certificateType: "none",
|
|
||||||
https: false,
|
|
||||||
path: "/",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return domain;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateWildcard = async (
|
|
||||||
input: typeof apiFindDomainByApplication._type,
|
|
||||||
) => {
|
|
||||||
const application = await findApplicationById(input.applicationId);
|
|
||||||
const admin = await findAdmin();
|
|
||||||
|
|
||||||
if (!admin.host) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "We need a host to generate a wildcard domain",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const domain = await createDomain({
|
|
||||||
applicationId: application.applicationId,
|
|
||||||
host: generateWildcardDomain(application.appName, admin.host || ""),
|
|
||||||
port: 3000,
|
|
||||||
certificateType: "none",
|
|
||||||
https: false,
|
|
||||||
path: "/",
|
|
||||||
});
|
|
||||||
|
|
||||||
return domain;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateWildcardDomain = (
|
export const generateWildcardDomain = (
|
||||||
@@ -114,6 +79,17 @@ export const findDomainsByApplicationId = async (applicationId: string) => {
|
|||||||
return domainsArray;
|
return domainsArray;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const findDomainsByComposeId = async (composeId: string) => {
|
||||||
|
const domainsArray = await db.query.domains.findMany({
|
||||||
|
where: eq(domains.composeId, composeId),
|
||||||
|
with: {
|
||||||
|
compose: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return domainsArray;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateDomainById = async (
|
export const updateDomainById = async (
|
||||||
domainId: string,
|
domainId: string,
|
||||||
domainData: Partial<Domain>,
|
domainData: Partial<Domain>,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const buildType = pgEnum("buildType", [
|
|||||||
"heroku_buildpacks",
|
"heroku_buildpacks",
|
||||||
"paketo_buildpacks",
|
"paketo_buildpacks",
|
||||||
"nixpacks",
|
"nixpacks",
|
||||||
|
"static",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// TODO: refactor this types
|
// TODO: refactor this types
|
||||||
@@ -140,6 +141,7 @@ export const applications = pgTable("application", {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
dockerfile: text("dockerfile"),
|
dockerfile: text("dockerfile"),
|
||||||
|
dockerContextPath: text("dockerContextPath"),
|
||||||
// Drop
|
// Drop
|
||||||
dropBuildPath: text("dropBuildPath"),
|
dropBuildPath: text("dropBuildPath"),
|
||||||
// Docker swarm json
|
// Docker swarm json
|
||||||
@@ -157,6 +159,7 @@ export const applications = pgTable("application", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.default("idle"),
|
.default("idle"),
|
||||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||||
|
publishDirectory: text("publishDirectory"),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
@@ -315,7 +318,9 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
"heroku_buildpacks",
|
"heroku_buildpacks",
|
||||||
"paketo_buildpacks",
|
"paketo_buildpacks",
|
||||||
"nixpacks",
|
"nixpacks",
|
||||||
|
"static",
|
||||||
]),
|
]),
|
||||||
|
publishDirectory: z.string().optional(),
|
||||||
owner: z.string(),
|
owner: z.string(),
|
||||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||||
@@ -352,8 +357,10 @@ export const apiSaveBuildType = createSchema
|
|||||||
applicationId: true,
|
applicationId: true,
|
||||||
buildType: true,
|
buildType: true,
|
||||||
dockerfile: true,
|
dockerfile: true,
|
||||||
|
dockerContextPath: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.merge(createSchema.pick({ publishDirectory: true }));
|
||||||
|
|
||||||
export const apiSaveGithubProvider = createSchema
|
export const apiSaveGithubProvider = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { sshKeys } from "@/server/db/schema/ssh-key";
|
import { sshKeys } from "@/server/db/schema/ssh-key";
|
||||||
import { generatePassword } from "@/templates/utils";
|
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { deployments } from "./deployment";
|
import { deployments } from "./deployment";
|
||||||
|
import { domains } from "./domain";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus } from "./shared";
|
||||||
@@ -72,6 +72,7 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
|||||||
fields: [compose.customGitSSHKeyId],
|
fields: [compose.customGitSSHKeyId],
|
||||||
references: [sshKeys.sshKeyId],
|
references: [sshKeys.sshKeyId],
|
||||||
}),
|
}),
|
||||||
|
domains: many(domains),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(compose, {
|
const createSchema = createInsertSchema(compose, {
|
||||||
@@ -106,6 +107,11 @@ export const apiFindCompose = z.object({
|
|||||||
composeId: z.string().min(1),
|
composeId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiFetchServices = z.object({
|
||||||
|
composeId: z.string().min(1),
|
||||||
|
type: z.enum(["fetch", "cache"]).optional().default("cache"),
|
||||||
|
});
|
||||||
|
|
||||||
export const apiUpdateCompose = createSchema.partial().extend({
|
export const apiUpdateCompose = createSchema.partial().extend({
|
||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
composeFile: z.string().optional(),
|
composeFile: z.string().optional(),
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
import { domain } from "@/server/db/validations";
|
import { domain } from "@/server/db/validations/domain";
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";
|
import {
|
||||||
|
boolean,
|
||||||
|
integer,
|
||||||
|
pgEnum,
|
||||||
|
pgTable,
|
||||||
|
serial,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
import { applications } from "./application";
|
import { applications } from "./application";
|
||||||
|
import { compose } from "./compose";
|
||||||
import { certificateType } from "./shared";
|
import { certificateType } from "./shared";
|
||||||
|
|
||||||
|
export const domainType = pgEnum("domainType", ["compose", "application"]);
|
||||||
|
|
||||||
export const domains = pgTable("domain", {
|
export const domains = pgTable("domain", {
|
||||||
domainId: text("domainId")
|
domainId: text("domainId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -13,15 +24,21 @@ export const domains = pgTable("domain", {
|
|||||||
.$defaultFn(() => nanoid()),
|
.$defaultFn(() => nanoid()),
|
||||||
host: text("host").notNull(),
|
host: text("host").notNull(),
|
||||||
https: boolean("https").notNull().default(false),
|
https: boolean("https").notNull().default(false),
|
||||||
port: integer("port").default(80),
|
port: integer("port").default(3000),
|
||||||
path: text("path").default("/"),
|
path: text("path").default("/"),
|
||||||
|
serviceName: text("serviceName"),
|
||||||
|
domainType: domainType("domainType").default("application"),
|
||||||
uniqueConfigKey: serial("uniqueConfigKey"),
|
uniqueConfigKey: serial("uniqueConfigKey"),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
applicationId: text("applicationId")
|
composeId: text("composeId").references(() => compose.composeId, {
|
||||||
.notNull()
|
onDelete: "cascade",
|
||||||
.references(() => applications.applicationId, { onDelete: "cascade" }),
|
}),
|
||||||
|
applicationId: text("applicationId").references(
|
||||||
|
() => applications.applicationId,
|
||||||
|
{ onDelete: "cascade" },
|
||||||
|
),
|
||||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,6 +47,10 @@ export const domainsRelations = relations(domains, ({ one }) => ({
|
|||||||
fields: [domains.applicationId],
|
fields: [domains.applicationId],
|
||||||
references: [applications.applicationId],
|
references: [applications.applicationId],
|
||||||
}),
|
}),
|
||||||
|
compose: one(compose, {
|
||||||
|
fields: [domains.composeId],
|
||||||
|
references: [compose.composeId],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
|
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
|
||||||
@@ -41,6 +62,9 @@ export const apiCreateDomain = createSchema.pick({
|
|||||||
https: true,
|
https: true,
|
||||||
applicationId: true,
|
applicationId: true,
|
||||||
certificateType: true,
|
certificateType: true,
|
||||||
|
composeId: true,
|
||||||
|
serviceName: true,
|
||||||
|
domainType: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiFindDomain = createSchema
|
export const apiFindDomain = createSchema
|
||||||
@@ -53,6 +77,14 @@ export const apiFindDomainByApplication = createSchema.pick({
|
|||||||
applicationId: true,
|
applicationId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiCreateTraefikMeDomain = createSchema.pick({}).extend({
|
||||||
|
appName: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiFindDomainByCompose = createSchema.pick({
|
||||||
|
composeId: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const apiUpdateDomain = createSchema
|
export const apiUpdateDomain = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
host: true,
|
host: true,
|
||||||
@@ -60,5 +92,7 @@ export const apiUpdateDomain = createSchema
|
|||||||
port: true,
|
port: true,
|
||||||
https: true,
|
https: true,
|
||||||
certificateType: true,
|
certificateType: true,
|
||||||
|
serviceName: true,
|
||||||
|
domainType: true,
|
||||||
})
|
})
|
||||||
.merge(createSchema.pick({ domainId: true }).required());
|
.merge(createSchema.pick({ domainId: true }).required());
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const users = pgTable("user", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
||||||
|
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
||||||
canCreateServices: boolean("canCreateServices").notNull().default(false),
|
canCreateServices: boolean("canCreateServices").notNull().default(false),
|
||||||
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
||||||
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
||||||
@@ -107,6 +108,7 @@ export const apiAssignPermissions = createSchema
|
|||||||
canAccessToTraefikFiles: true,
|
canAccessToTraefikFiles: true,
|
||||||
canAccessToDocker: true,
|
canAccessToDocker: true,
|
||||||
canAccessToAPI: true,
|
canAccessToAPI: true,
|
||||||
|
canAccessToSSHKeys: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
|||||||
46
apps/dokploy/server/db/validations/domain.ts
Normal file
46
apps/dokploy/server/db/validations/domain.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const domain = z
|
||||||
|
.object({
|
||||||
|
host: z.string().min(1, { message: "Add a hostname" }),
|
||||||
|
path: z.string().min(1).optional(),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.min(1, { message: "Port must be at least 1" })
|
||||||
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
|
.optional(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||||
|
})
|
||||||
|
.superRefine((input, ctx) => {
|
||||||
|
if (input.https && !input.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const domainCompose = z
|
||||||
|
.object({
|
||||||
|
host: z.string().min(1, { message: "Host is required" }),
|
||||||
|
path: z.string().min(1).optional(),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.min(1, { message: "Port must be at least 1" })
|
||||||
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
|
.optional(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||||
|
serviceName: z.string().min(1, { message: "Service name is required" }),
|
||||||
|
})
|
||||||
|
.superRefine((input, ctx) => {
|
||||||
|
if (input.https && !input.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -35,27 +35,3 @@ export const sshKeyUpdate = sshKeyCreate.pick({
|
|||||||
export const sshKeyType = z.object({
|
export const sshKeyType = z.object({
|
||||||
type: z.enum(["rsa", "ed25519"]).optional(),
|
type: z.enum(["rsa", "ed25519"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const domain = z
|
|
||||||
.object({
|
|
||||||
host: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/, {
|
|
||||||
message: "Invalid hostname",
|
|
||||||
}),
|
|
||||||
path: z.string().min(1).optional(),
|
|
||||||
port: z
|
|
||||||
.number()
|
|
||||||
.min(1, { message: "Port must be at least 1" })
|
|
||||||
.max(65535, { message: "Port must be 65535 or below" })
|
|
||||||
.optional(),
|
|
||||||
https: z.boolean().optional(),
|
|
||||||
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
|
||||||
})
|
|
||||||
.superRefine((input, ctx) => {
|
|
||||||
if (input.https && !input.certificateType) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
path: ["certificateType"],
|
|
||||||
message: "Required",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -36,10 +36,9 @@ export const initializePostgres = async () => {
|
|||||||
Ports: [
|
Ports: [
|
||||||
{
|
{
|
||||||
TargetPort: 5432,
|
TargetPort: 5432,
|
||||||
...(process.env.NODE_ENV === "development"
|
PublishedPort: process.env.NODE_ENV === "development" ? 5432 : 0,
|
||||||
? { PublishedPort: 5432 }
|
|
||||||
: {}),
|
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
|
PublishMode: "host",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user