Compare commits

...

92 Commits

Author SHA1 Message Date
Mauricio Siu
9f6f872536 Merge pull request #495 from Dokploy/canary
v0.9.1
2024-09-28 12:00:04 -06:00
Mauricio Siu
cb03b153ac Merge pull request #494 from Dokploy/fix/swagger-token
Fix/swagger token
2024-09-28 02:31:39 -06:00
Mauricio Siu
e5d7a0cb10 Merge pull request #493 from Dokploy/491-bug-nixpacks-publish-directory-issues-with-multi-level-paths
fix(nixpacks): adjust build path on nixpacks static
2024-09-28 02:30:29 -06:00
Mauricio Siu
bf65bc9462 chore(lint): format 2024-09-28 02:25:06 -06:00
Mauricio Siu
b48b9765cd fix(nixpacks): adjust build path on nixpacks static 2024-09-28 02:23:30 -06:00
Mauricio Siu
7cce02f74d Merge pull request #489 from Dokploy/docs/488-env-editor-not-support-multiline-variables
docs: add explanation how to use multiline env variables
2024-09-27 22:38:36 -06:00
Ben
3dc3672406 docs: add explanation how to use multiline env variables 2024-09-27 19:30:41 +02:00
Mauricio Siu
bbfe095045 chore(version): bump version 2024-09-27 11:23:44 -06:00
Mauricio Siu
65c1001751 fix(swagger): add mising validation 2024-09-27 11:22:52 -06:00
Mauricio Siu
5212bde021 Merge pull request #486 from Dokploy/333-add-flexible-wwwnon-www-redirect-option
#333 add flexible www/non-www redirect option
2024-09-27 00:42:30 -06:00
Ben
9059f42b03 refactor: display certificate select field after https switch field inside add-domain dialogs 2024-09-26 13:53:10 +02:00
Ben
3b9f5d6f5c feat: add presets for add-redirect dialog 2024-09-26 13:14:14 +02:00
Mauricio Siu
21dee4abac refactor: update social networks 2024-09-22 20:03:48 -06:00
Mauricio Siu
e378d89477 Merge pull request #475 from Dokploy/canary
v0.9.0
2024-09-22 19:38:12 -06:00
Mauricio Siu
b04c1206e4 refactor(multi-server): update logs 2024-09-22 19:28:19 -06:00
Mauricio Siu
639bc0e8db chore(version): bump version 2024-09-22 19:17:39 -06:00
Mauricio Siu
9a850d388d Merge pull request #453 from mpcref/patch-1
Generate valid appName on changing name
2024-09-22 19:07:09 -06:00
Mauricio Siu
6c5c374139 feat(docs): add multi server docs and troubleshooting 2024-09-22 18:54:32 -06:00
Mauricio Siu
0b05f8b83c Merge pull request #469 from Dokploy/139-multi-server-feature
139 multi server feature
2024-09-22 17:20:26 -06:00
Mauricio Siu
63d5b775e6 refactor(multi-server): add config 2024-09-22 17:18:07 -06:00
Mauricio Siu
cb16de63df refactor(multi-server): copy the right value 2024-09-22 17:12:28 -06:00
Mauricio Siu
31a4a0814e refactor: remove logs 2024-09-22 16:51:31 -06:00
Mauricio Siu
14302ed240 refactor: remove imports 2024-09-22 16:49:07 -06:00
Mauricio Siu
31c55f772d refactor(multi-server): remove logs 2024-09-22 16:37:57 -06:00
Mauricio Siu
f0f34df13c refactor(multi-server): improve alerts and add instructions to ssh keys 2024-09-22 13:57:13 -06:00
Mauricio Siu
1a877340d3 refactor(multi-server): delete server only if the server doesn't have associated services 2024-09-22 11:56:31 -06:00
Mauricio Siu
f7e43fa1c1 chore(migrations): apply single migration 2024-09-22 01:00:19 -06:00
Mauricio Siu
906906102b fix(backups): remove schedule when updating to prevent to create another cron job #452 2024-09-22 00:39:19 -06:00
Mauricio Siu
245a5175a8 refactor(compose): hide enviroment when is docker stack 2024-09-22 00:06:49 -06:00
Mauricio Siu
f427014f52 chore(lint): apply lint 2024-09-21 23:49:42 -06:00
Mauricio Siu
0465a71d86 feat(multi-server): enable docker terminal inside of containers 2024-09-21 23:49:00 -06:00
Mauricio Siu
3de8a18ef9 Merge branch 'canary' into 139-multi-server-feature 2024-09-21 22:55:15 -06:00
Mauricio Siu
e317d0c808 Merge branch 'canary' into 139-multi-server-feature 2024-09-21 22:52:44 -06:00
Mauricio Siu
ff482ffe28 refactor(multi-server): add docker cleanup cron 2024-09-21 22:47:19 -06:00
Mauricio Siu
82588f3e16 refactor(multi-server): add flag to queues 2024-09-21 21:57:53 -06:00
Mauricio Siu
069f1a7b7a refactor(multi-server): add serverIp 2024-09-21 21:17:03 -06:00
Mauricio Siu
807137d3b1 refactor(multi-server): add modals and separate server actions 2024-09-21 20:02:37 -06:00
Mauricio Siu
c03c154fc4 feat(multi-server): add docker containers view to servers 2024-09-21 15:16:15 -06:00
Mauricio Siu
698ff9e918 refactor(multi server): rename functions 2024-09-21 02:10:25 -06:00
Mauricio Siu
8bf6a22db8 feat(multi server): add env and toggle dashboard remote 2024-09-21 01:44:31 -06:00
Mauricio Siu
497d45129c feat(multi-server): add actions to the server 2024-09-21 00:06:41 -06:00
Mauricio Siu
0b22b694e6 refactor: add conditional to prevent access to terminal when is not server 2024-09-20 01:33:06 -06:00
Mauricio Siu
ee5516bb91 refactor(server): throw error when authentication fails 2024-09-20 00:15:25 -06:00
Mauricio Siu
e90b98e629 refactor: add warning when using keyboard interactive 2024-09-19 23:51:40 -06:00
Mauricio Siu
ff382d2029 Update installation.mdx 2024-09-19 23:33:36 -06:00
Mauricio Siu
4a37f85a51 chore: update dockerfile 2024-09-19 22:59:14 -06:00
Mauricio Siu
6bdc833413 chore: add unzip and update rclone installation command 2024-09-19 22:56:59 -06:00
Mauricio Siu
17a64a9402 refactor: add conditional to prevent have value in serverId 2024-09-19 22:51:18 -06:00
Mauricio Siu
a22b0797b1 refactor(dockerfile): remove sudo 2024-09-19 22:39:28 -06:00
Mauricio Siu
f3b351245a chore(ci/cd): add feature tag 2024-09-19 22:34:21 -06:00
Mauricio Siu
0cb74c5fde chore(license): update license 2024-09-19 22:28:29 -06:00
Mauricio Siu
9a828d4966 refactor(linter): format files 2024-09-19 22:25:25 -06:00
Mauricio Siu
4845c1ad5d refactor(multi-server): use rclone for databases backup local and external server 2024-09-19 21:56:45 -06:00
Mauricio Siu
6159786dfe feat(multi-server): add backups remote server 2024-09-19 21:20:25 -06:00
Mauricio Siu
b473062f40 Merge pull request #468 from max-degterev/canary
Missing return for entire domain creation transaction
2024-09-19 20:56:46 -06:00
Max Degterev
6c08f33ebb missing transaction return 2024-09-20 01:42:28 +02:00
Mauricio Siu
54f855e738 refactor(backups): wip postgres remote backups 2024-09-19 00:50:26 -06:00
Mauricio Siu
79f39db502 refactor(multi-server): wip remote backups 2024-09-18 23:03:54 -06:00
Mauricio Siu
a46e7759b2 refactor(multi-server): add rclone to multi server 2024-09-18 00:40:52 -06:00
Mauricio Siu
f001a50278 refactor(multi-server): show all the servers 2024-09-17 00:51:49 -06:00
Mauricio Siu
4c3bc8efdc refactor(deployment): set status error when application or compose have troubles when creating deployment log 2024-09-17 00:49:21 -06:00
Mauricio Siu
a591e02ffa refactor(multi-server): add deploy templates on different servers 2024-09-17 00:38:27 -06:00
Mauricio Siu
abe787593c refactor(multi-server): remove databases and hide monitoring when using deploy on server 2024-09-17 00:05:47 -06:00
Mauricio Siu
9b312cd9d7 refactor(multi-server): adapt paths on server and in dokploy ui 2024-09-16 23:49:24 -06:00
Mauricio Siu
c84d39a20f refactor: remove redis 2024-09-16 15:16:51 -06:00
Michiel Crefcoeur
847d6ecab1 add trailing comma's 2024-09-16 22:21:12 +02:00
Michiel Crefcoeur
8f83ecb9ef formatting 2024-09-16 22:20:02 +02:00
Michiel Crefcoeur
2f9448dde9 corrections 2024-09-16 22:13:30 +02:00
Michiel Crefcoeur
e1ec0aee69 replaceAll 2024-09-16 10:50:46 -07:00
Michiel Crefcoeur
7f378b12ae and for database 2024-09-16 07:37:23 -07:00
Michiel Crefcoeur
fac984d299 same thing for compose 2024-09-16 07:35:59 -07:00
Michiel Crefcoeur
4f3eb7b362 Generate valid appName on changing name
The same can probably also be done at other places. Should probably be solved in a generic way.
2024-09-16 07:25:16 -07:00
Mauricio Siu
d8d0b60cb3 refactor(multi-server): replace executeCommand with execAsyncRemote 2024-09-16 00:40:11 -06:00
Mauricio Siu
19295ba746 feat(multi server): add support for drag n drop 2024-09-15 17:05:17 -06:00
Mauricio Siu
0d3c978aad refactor(deployments): improve build error 2024-09-15 14:48:40 -06:00
Mauricio Siu
d2c8632c4f refactor(server): add support for multi server 2024-09-15 01:24:55 -06:00
Mauricio Siu
033bf66405 chore: add dokploy dashboard in readme 2024-09-13 13:49:42 -06:00
Mauricio Siu
c549ea17d8 refactor(multi-server): add error support 2024-09-13 01:06:46 -06:00
Mauricio Siu
c412dabc54 refactor(multi-server): fix deploy on docker compose 2024-09-13 01:03:38 -06:00
Mauricio Siu
0bd0da2ee4 fix(raw): add raw compose remote 2024-09-12 01:44:21 -06:00
Mauricio Siu
bf58ae0f0f feat(remote): add docker compose remote 2024-09-12 01:33:33 -06:00
Mauricio Siu
a8d714c20d feat: add multi server compose 2024-09-10 01:11:15 -06:00
Mauricio Siu
86f1bf31b8 feat: add reload, stop and start in remote server 2024-09-09 23:46:24 -06:00
Mauricio Siu
95f75fdccb feat: add application and databases external servers 2024-09-09 15:58:58 -06:00
Mauricio Siu
5afe1645a0 feat: add domains in external server 2024-09-09 09:35:02 -06:00
Mauricio Siu
cf06162be7 refactor: show logs to each application and database 2024-09-09 00:14:08 -06:00
Mauricio Siu
ea5349c844 feat: add logs for each application 2024-09-08 23:11:39 -06:00
Mauricio Siu
6007427a6c feat: add deploy databases to external server 2024-09-08 22:56:21 -06:00
Mauricio Siu
0a889c5db1 feat: deploy compose on external servers 2024-09-08 22:40:42 -06:00
Mauricio Siu
3d60236b36 feat: add redis installation on server and deploy applications on servers 2024-09-08 22:10:24 -06:00
Mauricio Siu
1a34ba175e refactor: simplify docker commands 2024-09-08 01:52:47 -06:00
Mauricio Siu
bd0bbdea26 feat: init multi server feature 2024-09-08 01:45:39 -06:00
214 changed files with 11666 additions and 3450 deletions

View File

@@ -27,7 +27,7 @@ WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl apache2-utils && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
@@ -42,7 +42,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules
# Install docker
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
@@ -55,4 +55,4 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
CMD [ "pnpm", "start" ]
CMD [ "pnpm", "start" ]

View File

@@ -74,7 +74,7 @@ export function generateMetadata({
},
twitter: {
card: "summary_large_image",
creator: "@siumauricio",
creator: "@getdokploy",
title: page.data.title,
description: page.data.description,
images: [

View File

@@ -15,6 +15,8 @@ Configure the source of your code, the way your application is built, and also m
If you need to assign environment variables to your application, you can do so here.
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
## Monitoring
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.

View File

@@ -26,6 +26,8 @@ Actions like deploying, updating, and deleting your database, and stopping it.
If you need to assign environment variables to your application, you can do so here.
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
## Monitoring
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.

View File

@@ -1,22 +1,23 @@
---
title: 'Comparison'
description: 'A comparison of Dokploy, CapRover, Dokku, and Coolify'
title: "Comparison"
description: "A comparison of Dokploy, CapRover, Dokku, and Coolify"
---
Comparison of the following deployment tools:
| Feature | Dokploy | CapRover | Dokku | Coolify |
|-----------------------------------|---------------------------------------|--------------------------------------|--------------------------------------|--------------------------------------|
| **User Interface** | ✅ | ✅ | ❌ | ✅ |
| **Docker compose support** | | ❌ | ❌ | ✅ |
| **API/CLI** | ✅ | ✅ | ✅ | ✅ |
| **Multi node support** | ✅ | ✅ | ❌ | ✅ |
| **Traefik Integration** | ✅ | ✅ | Available via Plugins | ✅ |
| **User Permission Management** | ✅ | ❌ | ❌ | ✅ |
| **Advanced User Permission Management** | ✅ | ❌ | ❌ | ❌ |
| **Terminal Access Built In** | ✅ | ❌ | ❌ | ✅ |
| **Database Support** | ✅ | ✅ | ❌ | ✅ |
| **Monitoring** | ✅ | ✅ | ❌ | ❌ |
| **Backups** | ✅ | Available via Plugins | Available via Plugins | ✅ |
| **Open Source** | ✅ | ✅ | ✅ | ✅ |
| **Cloud/Paid Version** | ❌ | ✅ | ❌ | ✅ |
| Feature | Dokploy | CapRover | Dokku | Coolify |
| --------------------------------------- | ------- | --------------------- | --------------------- | ------- |
| **User Interface** | ✅ | ✅ | ❌ | ✅ |
| **Docker compose support** || ❌ | ❌ | ✅ |
| **API/CLI** | ✅ | ✅ | ✅ | ✅ |
| **Multi node support** | ✅ | ✅ | ❌ | ✅ |
| **Traefik Integration** | ✅ | ✅ | Available via Plugins | ✅ |
| **User Permission Management** | ✅ | ❌ | ❌ | ✅ |
| **Advanced User Permission Management** | ✅ | ❌ | ❌ | ❌ |
| **Terminal Access Built In** | ✅ | ❌ | ❌ | ✅ |
| **Database Support** | ✅ | ✅ | ❌ | ✅ |
| **Monitoring** | ✅ | ✅ | ❌ | ❌ |
| **Backups** | ✅ | Available via Plugins | Available via Plugins | ✅ |
| **Open Source** | ✅ | ✅ | ✅ | ✅ |
| **Multi Server Support** | ✅ | ❌ | ❌ | ✅ |
| **Cloud/Paid Version** | ❌ | ✅ | ✅ | ✅ |

View File

@@ -29,7 +29,7 @@ We have tested on the following Linux Distros:
### Providers
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://hostinger.com?REFERRALCODE=1SIUMAURICI97)
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97)
- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get 200$ credits for free with this referral link: [Referral Link](https://m.do.co/c/db24efd43f35)
- [Hetzner](https://www.hetzner.com/cloud/) Get 20€ credits for free with this referral link: [Referral Link](https://hetzner.cloud/?ref=vou4fhxJ1W2D)
- [Linode](https://www.linode.com/es/pricing/#compute-shared)

View File

@@ -64,6 +64,9 @@
"docker/overview",
"---Monitoring---",
"monitoring/overview",
"---Multi Server---",
"multi-server/overview",
"multi-server/example",
"---Cluster---",
"cluster/overview",
"---Deployments---",

View File

@@ -0,0 +1,117 @@
---
title: Example
description: "Example to setup a remote server and deploy application in a VPS."
---
import { Callout } from "fumadocs-ui/components/callout";
Multi server allows you to deploy your apps remotely to different servers without needing to build and run them where the Dokploy UI is installed.
## Requirements
1. To install Dokploy UI, follow the [installation guide](en/docs/core/get-started/installation).
2. Create an SSH key by going to `/dashboard/settings/ssh-keys` and add a new key. Be sure to copy the public key.
<ImageZoom
src="/assets/ssh-keys.png"
alt="Architecture Diagram"
width={1000}
height={600}
className="rounded-lg"
/>
3. Decide which remote server to deploy your apps on. We recommend these reliable providers:
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% off with this [referral link](https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97).
- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get $200 credits for free with this [referral link](https://m.do.co/c/db24efd43f35).
- [Hetzner](https://www.hetzner.com/cloud/) Get €20 credits with this [referral link](https://hetzner.cloud/?ref=vou4fhxJ1W2D).
- [Linode](https://www.linode.com/es/pricing/#compute-shared).
- [Vultr](https://www.vultr.com/pricing/#cloud-compute).
- [Scaleway](https://www.scaleway.com/en/pricing/?tags=baremetal,available).
- [Google Cloud](https://cloud.google.com/).
- [AWS](https://aws.amazon.com/ec2/pricing/).
4. When creating the server, it should ask for SSH keys. Ideally, use your computer's public key and the key you generated in the previous step. Here's how to add the public key in Hostinger:
<ImageZoom
src="/assets/hostinger-add-sshkey.png"
alt="Adding SSH key"
width={1000}
height={600}
className="rounded-lg"
/>
<Callout>The steps are similar across other providers.</Callout>
5. Copy the servers IP address and ensure you know the username (often `root`). Fill in all fields and click `Create`.
<ImageZoom
src="/assets/multi-server-add-server.png"
alt="Add server"
width={1000}
height={600}
className="rounded-lg"
/>
6. To test connectivity, open the server dropdown and click `Enter Terminal`. If everything is correct, you should be able to interact with the remote server.
7. Click `Setup Server` to proceed. There are two tabs: SSH Keys and Deployments. This guide explains the easy way, but you can follow the manual process via the Dokploy UI if you prefer.
<ImageZoom
src="/assets/multi-server-setup-2.png"
alt="Setup process"
width={1000}
height={600}
className="rounded-lg"
/>
8. Click `Deployments`, then `Setup Server`. If everything is correct, you should see output similar to this:
<ImageZoom
src="/assets/multi-server-setup-3.png"
alt="Server setup output"
width={1000}
height={600}
className="rounded-lg"
/>
<Callout>
You only need to run this setup once. If Dokploy updates later, check the
release notes to see if rerunning this command is required.
</Callout>
9. You're ready to deploy your apps! Let's test it out:
<ImageZoom
src="/assets/multi-server-add-app.png"
alt="Add app"
width={1000}
height={600}
className="rounded-lg"
/>
10. To check which server an app belongs to, youll see the server name at the top. If no server is selected, it defaults to `Dokploy Server`. Click `Deploy` to start building your app on the remote server. You can check the `Logs` tab to see the build process. For this example, well use a test repo:
Repo: `https://github.com/Dokploy/examples.git`
Branch: `main`
Build Path: `/astro`
<ImageZoom
src="/assets/multi-server-setup-app.png"
alt="App setup"
width={1000}
height={600}
className="rounded-lg"
/>
11. Once the build is done, go to `Domains` and create a free domain. Just click `Create` and youre good to go! 🎊
{" "}
<ImageZoom
src="/assets/multi-server-finish.png"
alt="Finished setup"
width={1000}
height={600}
className="rounded-lg"
/>

View File

@@ -0,0 +1,29 @@
---
title: Overview
description: "Deploy your apps to multiple servers remotely."
---
import { Callout } from "fumadocs-ui/components/callout";
Multi server allows you to deploy your apps remotely to different servers without needing to build and run them where the Dokploy UI is installed.
To use the multi-server feature, you need to have Dokploy UI installed either locally or on a remote server. We recommend using a remote server for better connectivity, security, and isolation, for remote instances we install only a traefik instance.
If you plan to only deploy apps to remote servers and use Dokploy UI for managing deployments, Dokploy will use around 250 MB of RAM and minimal CPU, so a low-resource server should be sufficient.
All the features we have documented previously are supported by Dokploy Multi Server. The only feature not supported is remote server monitoring, due to performance reasons. However, all functionalities should work the same as when deploying on the same server where Dokploy UI is installed.
## Features
1. **Enter the terminal**: Allows you to access the terminal of the remote server.
2. **Setup Server**: Allows you to configure the remote server.
- **SSH Keys**: Steps to add SSH keys to the remote server.
- **Deployments**: Steps to configure the remote server for deploying applications.
3. **Edit Server**: Allows you to modify the remote server's details, such as SSH key, name, description, IP, etc.
4. **View Actions**: Lets you perform actions like managing the Traefik instance, storage, and activating Docker cleanup.
5. **Show Traefik File System**: Displays the contents of the remote server's directory.
6. **Show Docker Containers**: Shows the Docker containers running on the remote server.
<Callout>
Remote server monitoring is not supported due to performance reasons.
</Callout>

View File

@@ -3,4 +3,90 @@ title: Overview
description: Solve the most common problems that occur when using Dokploy.
---
WIP
## Applications Domain Not Working?
You see the deployment succeeded, and logs are running, but the domain isn't working? Here's what to check:
1. **Correct Port Mapping**: Ensure the domain is using the correct port for your application. For example, if you're using Next.js, the port should be `3000`, or for Laravel, it should be `8000`. If you change the app port, update the domain to reflect that.
2. **Avoid Using `Ports` in Advanced Settings**: Generally, there's no need to use the `Ports` feature unless you want to access your app via `IP:port`. Leaving this feature enabled may interfere with your domain.
3. **Let's Encrypt Certificates**: It's crucial to point the domain to your servers IP **before** adding it in Dokploy. If the domain is added first, the certificate wont be generated, and you may need to recreate the domain or restart Traefik.
4. **Listen on 0.0.0.0, Not 127.0.0.1**: If your app is bound to `127.0.0.1` (which is common in Vite apps), switch it to `0.0.0.0` to allow external access.
## Logs and Monitoring Not Working After Changing Application Placement?
This is expected behavior. If the application is running on a different node (worker), the UI wont have access to logs or monitoring, as they're not on the same node.
## Mounts Are Causing My Application Not to Run?
Docker Swarm won't run your application if there are invalid mounts, even if the deployment shows as successful. Double-check your mounts to ensure they are valid.
## Volumes in Docker Compose Not Working?
For Docker Compose, all file mounts defined in the `volumes` section will be stored in the `files` folder. This is the default directory structure:
## I added a volume to my docker compose, but is not finding the volume?
For docker compose all the file mounts you've created in the volumes section will be stored to files folder, this is the default structure of the docker compose.
```
/application-name
/code
/files
```
So instead of using this invalid way to mount a volume:
```yaml
volumes:
- "/folder:/path/in/container" ❌
```
You should use this format:
```yaml
volumes:
- "../files/my-database:/var/lib/mysql" ✅
- "../files/my-configs:/etc/my-app/config" ✅
```
## Logs Not Loading When Deploying to a Remote Server?
There are a few potential reasons for this:
1. **Slow Server:**: If the server is too slow, it may struggle to handle concurrent requests, leading to SSL handshake errors.
2. **Insufficient Disk Space:** If the server doesn't have enough disk space, the logs may not load.
## Docker Compose Domain Not Working?
When adding a domain in your Docker Compose file, its not necessary to expose the ports directly. Simply specify the port where your app is running. Exposing the ports can lead to conflicts with other applications or ports.
Example of what not to do:
```yaml
services:
app:
image: dokploy/dokploy:latest
ports:
- 3000:3000
```
Recommended approach:
```yaml
services:
app:
image: dokploy/dokploy:latest
ports:
- 3000
- 80
```
Then, when creating the domain in Dokploy, specify the service name and port, like this:
```yaml
domain: my-app.com
serviceName: app
port: 3000
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
## Additional Terms for Specific Features
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
For further inquiries or permissions, please contact us directly.

View File

@@ -1,6 +1,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import { APPLICATIONS_PATH } from "@/server/constants";
import { paths } from "@/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@/server/utils/builders";
import { unzipDrop } from "@/server/utils/builders/drop";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
@@ -11,11 +13,84 @@ if (typeof window === "undefined") {
globalThis.FileList = undici.FileList as any;
}
const baseApp: ApplicationNested = {
applicationId: "",
applicationStatus: "done",
appName: "",
autoDeploy: true,
serverId: "",
branch: null,
dockerBuildStage: "",
buildArgs: null,
buildPath: "/",
gitlabPathNamespace: "",
buildType: "nixpacks",
bitbucketBranch: "",
bitbucketBuildPath: "",
bitbucketId: "",
bitbucketRepository: "",
bitbucketOwner: "",
githubId: "",
gitlabProjectId: 0,
gitlabBranch: "",
gitlabBuildPath: "",
gitlabId: "",
gitlabRepository: "",
gitlabOwner: "",
command: null,
cpuLimit: null,
cpuReservation: null,
createdAt: "",
customGitBranch: "",
customGitBuildPath: "",
customGitSSHKeyId: null,
customGitUrl: "",
description: "",
dockerfile: null,
dockerImage: null,
dropBuildPath: null,
enabled: null,
env: null,
healthCheckSwarm: null,
labelsSwarm: null,
memoryLimit: null,
memoryReservation: null,
modeSwarm: null,
mounts: [],
name: "",
networkSwarm: null,
owner: null,
password: null,
placementSwarm: null,
ports: [],
projectId: "",
publishDirectory: null,
redirects: [],
refreshToken: "",
registry: null,
registryId: null,
replicas: 1,
repository: null,
restartPolicySwarm: null,
rollbackConfigSwarm: null,
security: [],
sourceType: "git",
subtitle: null,
title: null,
updateConfigSwarm: null,
username: null,
dockerContextPath: null,
};
//
vi.mock("@/server/constants", () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
// APPLICATIONS_PATH: "./__test__/drop/zips/output",
}));
describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths();
beforeAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
@@ -25,39 +100,42 @@ describe("unzipDrop using real zip files", () => {
});
it("should correctly extract a zip with a single root folder", async () => {
const appName = "single-file";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
baseApp.appName = "single-file";
// const appName = "single-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, appName);
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "test.txt")).toBe(true);
});
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
const appName = "folderwithfile";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
baseApp.appName = "folderwithfile";
// const appName = "folderwithfile";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, appName);
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
});
it("should correctly extract a zip with multiple root folders", async () => {
const appName = "two-folders";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
baseApp.appName = "two-folders";
// const appName = "two-folders";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, appName);
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
@@ -66,13 +144,14 @@ describe("unzipDrop using real zip files", () => {
});
it("should correctly extract a zip with a single root with a file", async () => {
const appName = "nested";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
baseApp.appName = "nested";
// const appName = "nested";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, appName);
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
@@ -82,13 +161,14 @@ describe("unzipDrop using real zip files", () => {
});
it("should correctly extract a zip with a single root with a folder", async () => {
const appName = "folder-with-sibling-file";
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
baseApp.appName = "folder-with-sibling-file";
// const appName = "folder-with-sibling-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, appName);
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });

View File

@@ -9,6 +9,7 @@ const baseApp: ApplicationNested = {
applicationStatus: "done",
appName: "",
autoDeploy: true,
serverId: "",
branch: null,
dockerBuildStage: "",
buildArgs: null,

View File

@@ -278,6 +278,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="px-4">
<AlertBlock type="info">
Changing settings such as placements may cause the logs/monitoring
to be unavailable.
</AlertBlock>
</div>
<Form {...form}>
<form

View File

@@ -19,6 +19,15 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -36,6 +45,36 @@ const AddRedirectchema = z.object({
type AddRedirect = z.infer<typeof AddRedirectchema>;
// Default presets
const redirectPresets = [
// {
// label: "Allow www & non-www.",
// redirect: {
// regex: "",
// permanent: false,
// replacement: "",
// },
// },
{
id: "to-www",
label: "Redirect to www",
redirect: {
regex: "^https?://(?:www.)?(.+)",
permanent: true,
replacement: "https://www.$${1}",
},
},
{
id: "to-non-www",
label: "Redirect to non-www",
redirect: {
regex: "^https?://www.(.+)",
permanent: true,
replacement: "https://$${1}",
},
},
];
interface Props {
applicationId: string;
children?: React.ReactNode;
@@ -43,9 +82,10 @@ interface Props {
export const AddRedirect = ({
applicationId,
children = <PlusIcon className="h-4 w-4" />,
children = <PlusIcon className="w-4 h-4" />,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [presetSelected, setPresetSelected] = useState("");
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } =
@@ -81,19 +121,36 @@ export const AddRedirect = ({
await utils.application.readTraefikConfig.invalidate({
applicationId,
});
setIsOpen(false);
onDialogToggle(false);
})
.catch(() => {
toast.error("Error to create the redirect");
});
};
const onDialogToggle = (open: boolean) => {
setIsOpen(open);
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
// setPresetSelected("");
// form.reset();
};
const onPresetSelect = (presetId: string) => {
const redirectPreset = redirectPresets.find(
(preset) => preset.id === presetId,
)?.redirect;
if (!redirectPreset) return;
const { regex, permanent, replacement } = redirectPreset;
form.reset({ regex, permanent, replacement }, { keepDefaultValues: true });
setPresetSelected(presetId);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
<DialogTrigger asChild>
<Button>{children}</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Redirects</DialogTitle>
<DialogDescription>
@@ -102,6 +159,24 @@ export const AddRedirect = ({
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="md:col-span-2">
<Label>Presets</Label>
<Select onValueChange={onPresetSelect} value={presetSelected}>
<SelectTrigger>
<SelectValue placeholder="No preset selected" />
</SelectTrigger>
<SelectContent>
{redirectPresets.map((preset) => (
<SelectItem key={preset.label} value={preset.id}>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Separator />
<Form {...form}>
<form
id="hook-form-add-redirect"
@@ -142,7 +217,7 @@ export const AddRedirect = ({
control={form.control}
name="permanent"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Permanent</FormLabel>
<FormDescription>

View File

@@ -7,7 +7,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { File } from "lucide-react";
import { File, Loader2 } from "lucide-react";
import React from "react";
import { UpdateTraefikConfig } from "./update-traefik-config";
interface Props {
@@ -15,7 +15,7 @@ interface Props {
}
export const ShowTraefikConfig = ({ applicationId }: Props) => {
const { data } = api.application.readTraefikConfig.useQuery(
const { data, isLoading } = api.application.readTraefikConfig.useQuery(
{
applicationId,
},
@@ -35,7 +35,12 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data === null ? (
{isLoading ? (
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center justify-center min-h-[10vh]">
Loading...
<Loader2 className="animate-spin" />
</span>
) : !data ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<File className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">

View File

@@ -7,7 +7,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { AlertTriangle, Package } from "lucide-react";
import { Package } from "lucide-react";
import React from "react";
import { AddVolumes } from "./add-volumes";
import { DeleteVolume } from "./delete-volume";

View File

@@ -11,8 +11,9 @@ interface Props {
logPath: string | null;
open: boolean;
onClose: () => void;
serverId?: string;
}
export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null);
@@ -21,7 +22,7 @@ export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => {

View File

@@ -25,7 +25,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
{ applicationId },
{
enabled: !!applicationId,
refetchInterval: 5000,
refetchInterval: 1000,
},
);
const [url, setUrl] = React.useState("");
@@ -110,6 +110,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
</div>
)}
<ShowDeployment
serverId={data?.serverId || ""}
open={activeLog !== null}
onClose={() => setActiveLog(null)}
logPath={activeLog}

View File

@@ -140,7 +140,7 @@ export const AddDomain = ({
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
@@ -175,6 +175,7 @@ export const AddDomain = ({
onClick={() => {
generateDomain({
appName: application?.appName || "",
serverId: application?.serverId || "",
})
.then((domain) => {
field.onChange(domain);
@@ -240,6 +241,29 @@ export const AddDomain = ({
);
}}
/>
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg 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>
)}
/>
{form.getValues().https && (
<FormField
control={form.control}
@@ -269,38 +293,12 @@ export const AddDomain = ({
)}
/>
)}
<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"
>
<Button isLoading={isLoading} form="hook-form" type="submit">
{dictionary.submit}
</Button>
</DialogFooter>

View File

@@ -45,14 +45,17 @@ export const DeployApplication = ({ applicationId }: Props) => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
toast.success("Deploying Application....");
await refetch();
await deploy({
applicationId,
}).catch(() => {
toast.error("Error to deploy Application");
});
})
.then(async () => {
toast.success("Application deployed succesfully");
await refetch();
})
.catch(() => {
toast.error("Error to deploy Application");
});
await refetch();
}}

View File

@@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
type="submit"
className="w-fit"
isLoading={isLoading}
disabled={!zip}
disabled={!zip || isLoading}
>
Deploy{" "}
</Button>

View File

@@ -66,7 +66,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
) : (
<StopApplication applicationId={applicationId} />
)}
<DockerTerminalModal appName={data?.appName || ""}>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -16,6 +16,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
export const DockerLogs = dynamic(
@@ -30,12 +31,14 @@ export const DockerLogs = dynamic(
interface Props {
appName: string;
serverId?: string;
}
export const ShowDockerLogs = ({ appName }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
@@ -62,7 +65,14 @@ export const ShowDockerLogs = ({ appName }: Props) => {
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
<SelectValue placeholder="Select a container" />
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -79,6 +89,7 @@ export const ShowDockerLogs = ({ appName }: Props) => {
</SelectContent>
</Select>
<DockerLogs
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
/>

View File

@@ -9,10 +9,16 @@ import { useEffect, useRef, useState } from "react";
interface Props {
logPath: string | null;
serverId?: string;
open: boolean;
onClose: () => void;
}
export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
export const ShowDeploymentCompose = ({
logPath,
open,
onClose,
serverId,
}: Props) => {
const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null);
@@ -21,7 +27,7 @@ export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => {

View File

@@ -111,6 +111,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
</div>
)}
<ShowDeploymentCompose
serverId={data?.serverId || ""}
open={activeLog !== null}
onClose={() => setActiveLog(null)}
logPath={activeLog}

View File

@@ -161,7 +161,7 @@ export const AddDomainCompose = ({
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
@@ -190,7 +190,7 @@ export const AddDomainCompose = ({
{errorServices?.message}
</AlertBlock>
)}
<div className="flex flex-row gap-4 w-full items-end">
<div className="flex flex-row items-end w-full gap-4">
<FormField
control={form.control}
name="serviceName"
@@ -310,6 +310,7 @@ export const AddDomainCompose = ({
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
serverId: compose?.serverId || "",
appName: compose?.appName || "",
})
.then((domain) => {
@@ -376,6 +377,29 @@ export const AddDomainCompose = ({
);
}}
/>
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg 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>
)}
/>
{https && (
<FormField
control={form.control}
@@ -405,28 +429,6 @@ export const AddDomainCompose = ({
)}
/>
)}
<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>

View File

@@ -75,7 +75,10 @@ export const ComposeActions = ({ composeId }: Props) => {
<StopCompose composeId={composeId} />
)}
<DockerTerminalModal appName={data?.appName || ""}>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -16,6 +16,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader, Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
export const DockerLogs = dynamic(
@@ -30,14 +31,20 @@ export const DockerLogs = dynamic(
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
}
export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
export const ShowDockerLogsCompose = ({
appName,
appType,
serverId,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
appType,
serverId,
},
{
enabled: !!appName,
@@ -64,7 +71,14 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
<SelectValue placeholder="Select a container" />
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -81,6 +95,7 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
</SelectContent>
</Select>
<DockerLogs
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
/>

View File

@@ -17,23 +17,27 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { DockerMonitoring } from "../../monitoring/docker/show";
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
}
export const ShowMonitoringCompose = ({
appName,
appType = "stack",
serverId,
}: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName: appName,
appType,
serverId,
},
{
enabled: !!appName,
@@ -46,7 +50,7 @@ export const ShowMonitoringCompose = ({
const [containerId, setContainerId] = useState<string | undefined>();
const { mutateAsync: restart, isLoading } =
const { mutateAsync: restart, isLoading: isRestarting } =
api.docker.restartContainer.useMutation();
useEffect(() => {
@@ -77,7 +81,14 @@ export const ShowMonitoringCompose = ({
value={containerAppName}
>
<SelectTrigger>
<SelectValue placeholder="Select a container" />
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -95,7 +106,7 @@ export const ShowMonitoringCompose = ({
</SelectContent>
</Select>
<Button
isLoading={isLoading}
isLoading={isRestarting}
onClick={async () => {
if (!containerId) return;
toast.success(`Restarting container ${containerAppName}`);

View File

@@ -11,12 +11,14 @@ import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
export const ShowContainerConfig = ({ containerId }: Props) => {
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,

View File

@@ -8,9 +8,14 @@ import "@xterm/xterm/css/xterm.css";
interface Props {
id: string;
containerId: string;
serverId?: string | null;
}
export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
export const DockerLogsId: React.FC<Props> = ({
id,
containerId,
serverId,
}) => {
const [term, setTerm] = React.useState<Terminal>();
const [lines, setLines] = React.useState<number>(40);
@@ -38,7 +43,7 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}`;
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl);
const fitAddon = new FitAddon();

View File

@@ -22,9 +22,14 @@ export const DockerLogsId = dynamic(
interface Props {
containerId: string;
children?: React.ReactNode;
serverId?: string | null;
}
export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
export const ShowDockerModalLogs = ({
containerId,
children,
serverId,
}: Props) => {
return (
<Dialog>
<DialogTrigger asChild>
@@ -41,7 +46,11 @@ export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
<DialogDescription>View the logs for {containerId}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 pt-2.5">
<DockerLogsId id="terminal" containerId={containerId || ""} />
<DockerLogsId
id="terminal"
containerId={containerId || ""}
serverId={serverId}
/>
</div>
</DialogContent>
</Dialog>

View File

@@ -114,11 +114,20 @@ export const columns: ColumnDef<Container>[] = [
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<ShowDockerModalLogs containerId={container.containerId}>
<ShowDockerModalLogs
containerId={container.containerId}
serverId={container.serverId}
>
View Logs
</ShowDockerModalLogs>
<ShowContainerConfig containerId={container.containerId} />
<DockerTerminalModal containerId={container.containerId}>
<ShowContainerConfig
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<DockerTerminalModal
containerId={container.containerId}
serverId={container.serverId || ""}
>
Terminal
</DockerTerminalModal>
</DropdownMenuContent>

View File

@@ -34,8 +34,15 @@ export type Container = NonNullable<
RouterOutputs["docker"]["getContainers"]
>[0];
export const ShowContainers = () => {
const { data, isLoading } = api.docker.getContainers.useQuery();
interface Props {
serverId?: string;
}
export const ShowContainers = ({ serverId }: Props) => {
const { data, isLoading } = api.docker.getContainers.useQuery({
serverId,
});
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
@@ -103,83 +110,99 @@ export const ShowContainers = () => {
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : data?.length === 0 ? (
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
No results.
</span>
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : (
<>No results.</>
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
))}
</TableHeader>
<TableBody>
{table?.getRowModel()?.rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : (
<>No results.</>
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="space-x-2 flex flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
{data && data?.length > 0 && (
<div className="flex items-center justify-end space-x-2 py-4">
<div className="space-x-2 flex flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -18,10 +18,15 @@ const Terminal = dynamic(
interface Props {
containerId: string;
serverId?: string;
children?: React.ReactNode;
}
export const DockerTerminalModal = ({ children, containerId }: Props) => {
export const DockerTerminalModal = ({
children,
containerId,
serverId,
}: Props) => {
return (
<Dialog>
<DialogTrigger asChild>
@@ -40,7 +45,11 @@ export const DockerTerminalModal = ({ children, containerId }: Props) => {
</DialogDescription>
</DialogHeader>
<Terminal id="terminal" containerId={containerId} />
<Terminal
id="terminal"
containerId={containerId}
serverId={serverId || ""}
/>
</DialogContent>
</Dialog>
);

View File

@@ -8,9 +8,14 @@ import { AttachAddon } from "@xterm/addon-attach";
interface Props {
id: string;
containerId: string;
serverId?: string;
}
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
export const DockerTerminal: React.FC<Props> = ({
id,
containerId,
serverId,
}) => {
const termRef = useRef(null);
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
useEffect(() => {
@@ -33,7 +38,7 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}`;
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl);

View File

@@ -13,6 +13,7 @@ import {
} from "@/components/ui/form";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer<
interface Props {
path: string;
serverId?: string;
}
export const ShowTraefikFile = ({ path }: Props) => {
const { data, refetch } = api.settings.readTraefikFile.useQuery(
export const ShowTraefikFile = ({ path, serverId }: Props) => {
const {
data,
refetch,
isLoading: isLoadingFile,
} = api.settings.readTraefikFile.useQuery(
{
path,
serverId,
},
{
enabled: !!path,
@@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => {
});
useEffect(() => {
if (data) {
form.reset({
traefikConfig: data || "",
});
}
form.reset({
traefikConfig: data || "",
});
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
@@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
await mutateAsync({
traefikConfig: data.traefikConfig,
path,
serverId,
})
.then(async () => {
toast.success("Traefik config Updated");
@@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => {
className="w-full relative z-[5]"
>
<div className="flex flex-col overflow-auto">
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormDescription className="break-all">
{path}
</FormDescription>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
{isLoadingFile ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
<Loader2 className="animate-spin size-8 text-muted-foreground" />
</div>
) : (
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormDescription className="break-all">
{path}
</FormDescription>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:
router-name:
rule: Host('domain.com')
@@ -116,31 +130,36 @@ routers:
tls: false
middlewares: []
`}
{...field}
/>
</FormControl>
{...field}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-8">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-8">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
)}
</div>
<div className="flex justify-end">
<Button isLoading={isLoading} disabled={canEdit} type="submit">
<Button
isLoading={isLoading}
disabled={canEdit || isLoading}
type="submit"
>
Update
</Button>
</div>

View File

@@ -1,20 +1,47 @@
import React from "react";
import { AlertBlock } from "@/components/shared/alert-block";
import { Tree } from "@/components/ui/file-tree";
import { api } from "@/utils/api";
import { FileIcon, Folder, Workflow } from "lucide-react";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
import React from "react";
import { ShowTraefikFile } from "./show-traefik-file";
export const ShowTraefikSystem = () => {
interface Props {
serverId?: string;
}
export const ShowTraefikSystem = ({ serverId }: Props) => {
const [file, setFile] = React.useState<null | string>(null);
const { data: directories } = api.settings.readDirectories.useQuery();
const {
data: directories,
isLoading,
error,
isError,
} = api.settings.readDirectories.useQuery(
{
serverId,
},
{
retry: 2,
},
);
return (
<div className={cn("mt-6 md:grid gap-4")}>
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
{isError && (
<AlertBlock type="error" className="w-full">
{error?.message}
</AlertBlock>
)}
{isLoading && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
<Loader2 className="animate-spin size-8 text-muted-foreground" />
</div>
)}
{directories?.length === 0 && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
@@ -34,7 +61,7 @@ export const ShowTraefikSystem = () => {
/>
<div className="w-full">
{file ? (
<ShowTraefikFile path={file} />
<ShowTraefikFile path={file} serverId={serverId} />
) : (
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
<span className="text-muted-foreground text-lg font-medium">

View File

@@ -36,7 +36,10 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
) : (
<StopMariadb mariadbId={mariadbId} />
)}
<DockerTerminalModal appName={data?.appName || ""}>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -34,7 +34,10 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
) : (
<StopMongo mongoId={mongoId} />
)}
<DockerTerminalModal appName={data?.appName || ""}>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -23,7 +23,7 @@ export const DockerMemoryChart = ({
return {
time: item.time,
name: `Point ${index + 1}`,
usage: (item.value.used / 1024).toFixed(2),
usage: (item.value.used / 1024 ** 3).toFixed(2),
};
});
return (

View File

@@ -208,9 +208,7 @@ export const DockerMonitoring = ({
<div className="flex flex-col gap-2 w-full ">
<span className="text-base font-medium">Memory</span>
<span className="text-sm text-muted-foreground">
{`Used: ${(currentData.memory.value.used / 1024).toFixed(
2,
)} GB / Limit: ${(currentData.memory.value.total / 1024).toFixed(2)} GB`}
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
</span>
<Progress
value={currentData.memory.value.usedPercentage}
@@ -218,7 +216,7 @@ export const DockerMonitoring = ({
/>
<DockerMemoryChart
acummulativeData={acummulativeData.memory}
memoryLimitGB={currentData.memory.value.total / 1024}
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
/>
</div>
{appName === "dokploy" && (
@@ -240,9 +238,9 @@ export const DockerMonitoring = ({
<div className="flex flex-col gap-2 w-full ">
<span className="text-base font-medium">Block I/O</span>
<span className="text-sm text-muted-foreground">
{`Used: ${currentData.block.value.readMb.toFixed(
{`Read: ${currentData.block.value.readMb.toFixed(
2,
)} MB / Limit: ${currentData.block.value.writeMb.toFixed(
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
3,
)} MB`}
</span>

View File

@@ -35,7 +35,10 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<StopMysql mysqlId={mysqlId} />
)}
<DockerTerminalModal appName={data?.appName || ""}>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -38,7 +38,10 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<StopPostgres postgresId={postgresId} />
)}
<DockerTerminalModal appName={data?.appName || ""}>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -19,11 +19,26 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder } from "lucide-react";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -43,6 +58,7 @@ const AddTemplateSchema = z.object({
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
description: z.string().optional(),
serverId: z.string().optional(),
});
type AddTemplate = z.infer<typeof AddTemplateSchema>;
@@ -54,8 +70,10 @@ interface Props {
export const AddApplication = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation();
@@ -75,6 +93,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
appName: data.appName,
description: data.description,
projectId,
serverId: data.serverId,
})
.then(async () => {
toast.success("Service Created");
@@ -126,7 +145,10 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
form.setValue(
"appName",
`${slug}-${val.toLowerCase().replaceAll(" ", "-")}`,
);
field.onChange(val);
}}
/>
@@ -135,6 +157,57 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If not server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"

View File

@@ -22,15 +22,23 @@ import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, Folder } from "lucide-react";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -51,6 +59,7 @@ const AddComposeSchema = z.object({
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
description: z.string().optional(),
serverId: z.string().optional(),
});
type AddCompose = z.infer<typeof AddComposeSchema>;
@@ -63,6 +72,7 @@ interface Props {
export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
@@ -87,6 +97,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
projectId,
composeType: data.composeType,
appName: data.appName,
serverId: data.serverId,
})
.then(async () => {
toast.success("Compose Created");
@@ -138,7 +149,10 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
form.setValue(
"appName",
`${slug}-${val.toLowerCase()}`,
);
field.onChange(val);
}}
/>
@@ -148,6 +162,57 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)}
/>
</div>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If not server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"

View File

@@ -26,6 +26,15 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
@@ -71,6 +80,7 @@ const baseDatabaseSchema = z.object({
databasePassword: z.string(),
dockerImage: z.string(),
description: z.string().nullable(),
serverId: z.string().nullable(),
});
const mySchema = z.discriminatedUnion("type", [
@@ -145,6 +155,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const postgresMutation = api.postgres.create.useMutation();
const mongoMutation = api.mongo.create.useMutation();
const redisMutation = api.redis.create.useMutation();
@@ -161,6 +172,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
description: "",
databaseName: "",
databaseUser: "",
serverId: null,
},
resolver: zodResolver(mySchema),
});
@@ -183,6 +195,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
appName: data.appName,
dockerImage: defaultDockerImage,
projectId,
serverId: data.serverId,
description: data.description,
};
@@ -191,8 +204,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "mongo") {
promise = mongoMutation.mutateAsync({
@@ -200,11 +215,13 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databasePassword: data.databasePassword,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "redis") {
promise = redisMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
serverId: data.serverId,
projectId,
});
} else if (data.type === "mariadb") {
@@ -215,6 +232,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "mysql") {
promise = mysqlMutation.mutateAsync({
@@ -224,6 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
databaseRootPassword: data.databaseRootPassword,
serverId: data.serverId,
});
}
@@ -342,7 +361,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
form.setValue(
"appName",
`${slug}-${val.toLowerCase()}`,
);
field.onChange(val);
}}
/>
@@ -352,6 +374,39 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a Server</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"

View File

@@ -29,11 +29,27 @@ import {
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { ScrollArea } from "@radix-ui/react-scroll-area";
@@ -43,6 +59,7 @@ import {
Code,
Github,
Globe,
HelpCircle,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
@@ -58,9 +75,12 @@ export const AddTemplate = ({ projectId }: Props) => {
const [open, setOpen] = useState(false);
const { data } = api.compose.templates.useQuery();
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: tags, isLoading: isLoadingTags } =
api.compose.getTags.useQuery();
const utils = api.useUtils();
const [serverId, setServerId] = useState<string | undefined>(undefined);
const { mutateAsync, isLoading, error, isError } =
api.compose.deployTemplate.useMutation();
@@ -109,7 +129,6 @@ export const AddTemplate = ({ projectId }: Props) => {
role="combobox"
className={cn(
"md:max-w-[15rem] w-full justify-between !bg-input",
// !field.value && "text-muted-foreground",
)}
>
{isLoadingTags
@@ -268,30 +287,79 @@ export const AddTemplate = ({ projectId }: Props) => {
{template.name} template and add it to your
project.
</AlertDialogDescription>
<div>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If not server is selected, the
application will be deployed on the
server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={(e) => {
setServerId(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isLoading}
onClick={async () => {
await mutateAsync({
const promise = mutateAsync({
projectId,
serverId: serverId || undefined,
id: template.id,
})
.then(async () => {
toast.success(
`Succesfully created ${template.name} application from template`,
);
});
toast.promise(promise, {
loading: "Setting up...",
success: (data) => {
utils.project.one.invalidate({
projectId,
});
setOpen(false);
})
.catch(() => {
toast.error(
`Error creating ${template.name} application from template`,
);
});
return `${template.name} template created succesfully`;
},
error: (err) => {
return `Ocurred an error deploying ${template.name} template`;
},
});
}}
>
Confirm

View File

@@ -17,6 +17,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
@@ -109,6 +110,7 @@ export const AddProject = () => {
)}
/>
</div>
<FormField
control={form.control}
name="description"

View File

@@ -37,7 +37,10 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<StopRedis redisId={redisId} />
)}
<DockerTerminalModal appName={data?.appName || ""}>
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal

View File

@@ -19,10 +19,8 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { Edit } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -0,0 +1,52 @@
import { Button } from "@/components/ui/button";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
import { ShowModalLogs } from "../../web-server/show-modal-logs";
export const ShowDokployActions = () => {
const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}>
<Button isLoading={isLoading} variant="outline">
Server
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadServer()
.then(async () => {
toast.success("Server Reloaded");
})
.catch(() => {
toast.success("Server Reloaded");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy">
<span>Watch logs</span>
</ShowModalLogs>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,44 @@
import { CardDescription, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { ShowStorageActions } from "./show-storage-actions";
import { ShowTraefikActions } from "./show-traefik-actions";
import { ToggleDockerCleanup } from "./toggle-docker-cleanup";
interface Props {
serverId: string;
}
export const ShowServerActions = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
View Actions
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen ">
<div className="flex flex-col gap-1">
<DialogTitle className="text-xl">Web server settings</DialogTitle>
<DialogDescription>Reload or clean the web server.</DialogDescription>
</div>
<div className="grid grid-cols-2 w-full gap-4">
<ShowTraefikActions serverId={serverId} />
<ShowStorageActions serverId={serverId} />
<ToggleDockerCleanup serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,177 @@
import { Button } from "@/components/ui/button";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
serverId?: string;
}
export const ShowStorageActions = ({ serverId }: Props) => {
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
const {
mutateAsync: cleanDockerBuilder,
isLoading: cleanDockerBuilderIsLoading,
} = api.settings.cleanDockerBuilder.useMutation();
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
api.settings.cleanMonitoring.useMutation();
const {
mutateAsync: cleanUnusedImages,
isLoading: cleanUnusedImagesIsLoading,
} = api.settings.cleanUnusedImages.useMutation();
const {
mutateAsync: cleanUnusedVolumes,
isLoading: cleanUnusedVolumesIsLoading,
} = api.settings.cleanUnusedVolumes.useMutation();
const {
mutateAsync: cleanStoppedContainers,
isLoading: cleanStoppedContainersIsLoading,
} = api.settings.cleanStoppedContainers.useMutation();
return (
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
>
<Button
isLoading={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
variant="outline"
>
Space
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedImages({
serverId: serverId,
})
.then(async () => {
toast.success("Cleaned images");
})
.catch(() => {
toast.error("Error to clean images");
});
}}
>
<span>Clean unused images</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedVolumes({
serverId: serverId,
})
.then(async () => {
toast.success("Cleaned volumes");
})
.catch(() => {
toast.error("Error to clean volumes");
});
}}
>
<span>Clean unused volumes</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanStoppedContainers({
serverId: serverId,
})
.then(async () => {
toast.success("Stopped containers cleaned");
})
.catch(() => {
toast.error("Error to clean stopped containers");
});
}}
>
<span>Clean stopped containers</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanDockerBuilder({
serverId: serverId,
})
.then(async () => {
toast.success("Cleaned Docker Builder");
})
.catch(() => {
toast.error("Error to clean Docker Builder");
});
}}
>
<span>Clean Docker Builder & System</span>
</DropdownMenuItem>
{!serverId && (
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanMonitoring()
.then(async () => {
toast.success("Cleaned Monitoring");
})
.catch(() => {
toast.error("Error to clean Monitoring");
});
}}
>
<span>Clean Monitoring </span>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanAll({
serverId: serverId,
})
.then(async () => {
toast.success("Cleaned all");
})
.catch(() => {
toast.error("Error to clean all");
});
}}
>
<span>Clean all</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,125 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
import { ShowModalLogs } from "../../web-server/show-modal-logs";
interface Props {
serverId?: string;
}
export const ShowTraefikActions = ({ serverId }: Props) => {
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
api.settings.toggleDashboard.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery({
serverId,
});
return (
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
>
<Button
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
variant="outline"
>
Traefik
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadTraefik({
serverId: serverId,
})
.then(async () => {
toast.success("Traefik Reloaded");
})
.catch(() => {
toast.error("Error to reload the traefik");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
<span>Watch logs</span>
</ShowModalLogs>
<EditTraefikEnv serverId={serverId}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>Modify Env</span>
</DropdownMenuItem>
</EditTraefikEnv>
<DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard: !haveTraefikDashboardPortEnabled,
serverId: serverId,
})
.then(async () => {
toast.success(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
refetchDashboard();
})
.catch(() => {
toast.error(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
});
}}
className="w-full cursor-pointer space-x-3"
>
<span>
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
</span>
</DropdownMenuItem>
{/*
<DockerTerminalModal appName="dokploy-traefik">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<span>Enter the terminal</span>
</DropdownMenuItem>
</DockerTerminalModal> */}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,52 @@
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
serverId?: string;
}
export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.admin.one.useQuery(undefined, {
enabled: !serverId,
});
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
{
serverId: serverId || "",
},
{
enabled: !!serverId,
},
);
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
return (
<div className="flex items-center gap-4">
<Switch
checked={enabled}
onCheckedChange={async (e) => {
await mutateAsync({
enableDockerCleanup: e,
serverId: serverId,
})
.then(async () => {
toast.success("Docker Cleanup Enabled");
})
.catch(() => {
toast.error("Docker Cleanup Error");
});
if (serverId) {
refetchServer();
} else {
refetch();
}
}}
/>
<Label className="text-primary">Daily Docker Cleanup</Label>
</div>
);
};

View File

@@ -0,0 +1,253 @@
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,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
description: z.string().optional(),
ipAddress: z.string().min(1, {
message: "IP Address is required",
}),
port: z.number().optional(),
username: z.string().optional(),
sshKeyId: z.string().min(1, {
message: "SSH Key is required",
}),
});
type Schema = z.infer<typeof Schema>;
export const AddServer = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data: sshKeys } = api.sshKey.all.useQuery();
const { mutateAsync, error, isError } = api.server.create.useMutation();
const form = useForm<Schema>({
defaultValues: {
description: "",
name: "",
ipAddress: "",
port: 22,
username: "root",
sshKeyId: "",
},
resolver: zodResolver(Schema),
});
useEffect(() => {
form.reset({
description: "",
name: "",
ipAddress: "",
port: 22,
username: "root",
sshKeyId: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
name: data.name,
description: data.description || "",
ipAddress: data.ipAddress || "",
port: data.port || 22,
username: data.username || "root",
sshKeyId: data.sshKeyId || "",
})
.then(async (data) => {
await utils.server.all.invalidate();
toast.success("Server Created");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create a server");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>
<PlusIcon className="h-4 w-4" />
Create Server
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-3xl ">
<DialogHeader>
<DialogTitle>Add Server</DialogTitle>
<DialogDescription>
Add a server to deploy your applications remotely.
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-server"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4 ">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Hostinger Server" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="This server is for databases..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="sshKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a SSH Key</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a SSH Key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectLabel>
Registries ({sshKeys?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="ipAddress"
render={({ field }) => (
<FormItem>
<FormLabel>IP Address</FormLabel>
<FormControl>
<Input placeholder="192.168.1.100" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input placeholder="22" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="root" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
<DialogFooter>
<Button
isLoading={form.formState.isSubmitting}
form="hook-form-add-server"
type="submit"
>
Create
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,301 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import {
CopyIcon,
ExternalLinkIcon,
RocketIcon,
ServerIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { ShowDeployment } from "../../application/deployments/show-deployment";
interface Props {
serverId: string;
}
export const SetupServer = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data: server } = api.server.one.useQuery(
{
serverId,
},
{
enabled: !!serverId,
},
);
const [activeLog, setActiveLog] = useState<string | null>(null);
const { data: deployments, refetch } = api.deployment.allByServer.useQuery(
{ serverId },
{
enabled: !!serverId,
},
);
const { mutateAsync, isLoading } = api.server.setup.useMutation();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Setup Server
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-4xl overflow-y-auto max-h-screen ">
<DialogHeader>
<div className="flex flex-col gap-1.5">
<DialogTitle className="flex items-center gap-2">
<ServerIcon className="size-5" /> Setup Server
</DialogTitle>
<p className="text-muted-foreground text-sm">
To setup a server, please click on the button below.
</p>
</div>
</DialogHeader>
{!server?.sshKeyId ? (
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
<AlertBlock type="warning">
Please add a SSH Key to your server before setting up the server.
you can assign a SSH Key to your server in Edit Server.
</AlertBlock>
</div>
) : (
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
<Tabs defaultValue="ssh-keys">
<TabsList className="grid grid-cols-2 w-[400px]">
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
</TabsList>
<TabsContent
value="ssh-keys"
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
>
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
<p className="text-primary text-base font-semibold">
You have two options to add SSH Keys to your server:
</p>
<ul>
<li>
1. Add the public SSH Key when you create a server in your
preffered provider (Hostinger, Digital Ocean, Hetzner,
etc){" "}
</li>
<li>2. Add The SSH Key to Server Manually</li>
</ul>
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex relative flex-col gap-2 overflow-y-auto">
<div className="text-sm text-primary flex flex-row gap-2 items-center">
Copy Public Key ({server?.sshKey?.name})
<button
type="button"
className=" right-2 top-8"
onClick={() => {
copy(
server?.sshKey?.publicKey || "Generate a SSH Key",
);
toast.success("SSH Copied to clipboard");
}}
>
<CopyIcon className="size-4 text-muted-foreground" />
</button>
</div>
</div>
</div>
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4">
<span className="text-base font-semibold text-primary">
Automatic process
</span>
<Link
href="https://docs.dokploy.com/en/docs/core/get-started/introduction"
target="_blank"
className="text-primary flex flex-row gap-2"
>
View Tutorial <ExternalLinkIcon className="size-4" />
</Link>
</div>
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary">
Manual process
</span>
<ul>
<li className="items-center flex gap-1">
1. Login to your server{" "}
<span className="text-primary bg-secondary p-1 rounded-lg">
ssh {server?.username}@{server?.ipAddress}
</span>
<button
type="button"
onClick={() => {
copy(
`ssh ${server?.username}@${server?.ipAddress}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="size-4" />
</button>
</li>
<li>
2. When you are logged in run the following command
<div className="flex relative flex-col gap-4 w-full mt-2">
<CodeEditor
lineWrapping
language="properties"
value={`echo "${server?.sshKey?.publicKey}" >> ~/.ssh/authorized_keys`}
readOnly
className="font-mono opacity-60"
/>
<button
type="button"
className="absolute right-2 top-2"
onClick={() => {
copy(
`echo "${server?.sshKey?.publicKey}" >> ~/.ssh/authorized_keys`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="size-4" />
</button>
</div>
</li>
<li className="mt-1">
3. You're done, you can test the connection by entering
to the terminal or by setting up the server tab.
</li>
</ul>
</div>
</div>
</TabsContent>
<TabsContent value="deployments">
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex flex-row gap-2 justify-between w-full items-end max-sm:flex-col">
<div className="flex flex-col gap-1">
<CardTitle className="text-xl">
Deployments
</CardTitle>
<CardDescription>
See all the 5 Server Setup
</CardDescription>
</div>
<DialogAction
title={"Setup Server?"}
description="This will setup the server and all associated data"
onClick={async () => {
await mutateAsync({
serverId: server?.serverId || "",
})
.then(async () => {
refetch();
toast.success("Server setup successfully");
})
.catch(() => {
toast.error("Error configuring server");
});
}}
>
<Button isLoading={isLoading}>Setup Server</Button>
</DialogAction>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{server?.deployments?.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<RocketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No deployments found
</span>
</div>
) : (
<div className="flex flex-col gap-4">
{deployments?.map((deployment) => (
<div
key={deployment.deploymentId}
className="flex items-center justify-between rounded-lg border p-4 gap-2"
>
<div className="flex flex-col">
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{deployment.status}
<StatusTooltip
status={deployment?.status}
className="size-2.5"
/>
</span>
<span className="text-sm text-muted-foreground">
{deployment.title}
</span>
{deployment.description && (
<span className="break-all text-sm text-muted-foreground">
{deployment.description}
</span>
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground">
<DateTooltip date={deployment.createdAt} />
</div>
<Button
onClick={() => {
setActiveLog(deployment.logPath);
}}
>
View
</Button>
</div>
</div>
))}
</div>
)}
<ShowDeployment
open={activeLog !== null}
onClose={() => setActiveLog(null)}
logPath={activeLog}
/>
</CardContent>
</Card>
</div>
</CardContent>
</TabsContent>
</Tabs>
</div>
)}
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,48 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ContainerIcon } from "lucide-react";
import { useState } from "react";
import { ShowContainers } from "../../docker/show/show-containers";
interface Props {
serverId: string;
}
export const ShowDockerContainersModal = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Docker Containers
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
<DialogHeader>
<div className="flex flex-col gap-1.5">
<DialogTitle className="flex items-center gap-2">
<ContainerIcon className="size-5" /> Docker Containers
</DialogTitle>
<p className="text-muted-foreground text-sm">
See all the containers of your remote server
</p>
</div>
</DialogHeader>
<div className="grid w-full gap-1">
<ShowContainers serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,216 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
import { format } from "date-fns";
import { KeyIcon, MoreHorizontal, ServerIcon } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions";
import { AddServer } from "./add-server";
import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { UpdateServer } from "./update-server";
export const ShowServers = () => {
const { data, refetch } = api.server.all.useQuery();
const { mutateAsync } = api.server.remove.useMutation();
const { data: sshKeys } = api.sshKey.all.useQuery();
return (
<div className="p-6 space-y-6">
<div className="space-y-2 flex flex-row justify-between items-end">
<div>
<h1 className="text-2xl font-bold">Servers</h1>
<p className="text-muted-foreground">
Add servers to deploy your applications remotely.
</p>
</div>
{sshKeys && sshKeys?.length > 0 && (
<div>
<AddServer />
</div>
)}
</div>
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-1">
{sshKeys?.length === 0 && data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<KeyIcon className="size-8" />
<span className="text-base text-muted-foreground">
No SSH Keys found. Add a SSH Key to start adding servers.{" "}
<Link
href="/dashboard/settings/ssh-keys"
className="text-primary"
>
Add SSH Key
</Link>
</span>
</div>
) : (
data &&
data.length === 0 && (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<ServerIcon className="size-8" />
<span className="text-base text-muted-foreground">
No Servers found. Add a server to deploy your applications
remotely.
</span>
</div>
)
)}
{data && data?.length > 0 && (
<div className="flex flex-col gap-6">
<Table>
<TableCaption>See all servers</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Name</TableHead>
<TableHead className="text-center">IP Address</TableHead>
<TableHead className="text-center">Port</TableHead>
<TableHead className="text-center">Username</TableHead>
<TableHead className="text-center">SSH Key</TableHead>
<TableHead className="text-center">Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((server) => {
const canDelete = server.totalSum === 0;
return (
<TableRow key={server.serverId}>
<TableCell className="w-[100px]">{server.name}</TableCell>
<TableCell className="text-center">
<Badge>{server.ipAddress}</Badge>
</TableCell>
<TableCell className="text-center">
{server.port}
</TableCell>
<TableCell className="text-center">
{server.username}
</TableCell>
<TableCell className="text-right">
<span className="text-sm text-muted-foreground">
{server.sshKeyId ? "Yes" : "No"}
</span>
</TableCell>
<TableCell className="text-right">
<span className="text-sm text-muted-foreground">
{format(new Date(server.createdAt), "PPpp")}
</span>
</TableCell>
<TableCell className="text-right flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
{server.sshKeyId && (
<TerminalModal serverId={server.serverId}>
<span>Enter the terminal</span>
</TerminalModal>
)}
<SetupServer serverId={server.serverId} />
<UpdateServer serverId={server.serverId} />
{server.sshKeyId && (
<ShowServerActions serverId={server.serverId} />
)}
<DialogAction
disabled={!canDelete}
title={
canDelete
? "Delete Server"
: "Server has active services"
}
description={
canDelete ? (
"This will delete the server and all associated data"
) : (
<div className="flex flex-col gap-2">
You can not delete this server because it
has active services.
<AlertBlock type="warning">
You have active services associated with
this server, please delete them first.
</AlertBlock>
</div>
)
}
onClick={async () => {
await mutateAsync({
serverId: server.serverId,
})
.then(() => {
refetch();
toast.success(
`Server ${server.name} deleted succesfully`,
);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
Delete Server
</DropdownMenuItem>
</DialogAction>
{server.sshKeyId && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,48 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { FileTextIcon } from "lucide-react";
import { useState } from "react";
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
interface Props {
serverId: string;
}
export const ShowTraefikFileSystemModal = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Traefik File System
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
<DialogHeader>
<div className="flex flex-col gap-1.5">
<DialogTitle className="flex items-center gap-2">
<FileTextIcon className="size-5" /> Traefik File System
</DialogTitle>
<p className="text-muted-foreground text-sm">
See all the files and directories of your traefik configuration
</p>
</div>
</DialogHeader>
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
<ShowTraefikSystem serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,269 @@
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 { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
description: z.string().optional(),
ipAddress: z.string().min(1, {
message: "IP Address is required",
}),
port: z.number().optional(),
username: z.string().optional(),
sshKeyId: z.string().min(1, {
message: "SSH Key is required",
}),
});
type Schema = z.infer<typeof Schema>;
interface Props {
serverId: string;
}
export const UpdateServer = ({ serverId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data, isLoading } = api.server.one.useQuery(
{
serverId,
},
{
enabled: !!serverId,
},
);
const { data: sshKeys } = api.sshKey.all.useQuery();
const { mutateAsync, error, isError } = api.server.update.useMutation();
const form = useForm<Schema>({
defaultValues: {
description: "",
name: "",
ipAddress: "",
port: 22,
username: "root",
sshKeyId: "",
},
resolver: zodResolver(Schema),
});
useEffect(() => {
form.reset({
description: data?.description || "",
name: data?.name || "",
ipAddress: data?.ipAddress || "",
port: data?.port || 22,
username: data?.username || "root",
sshKeyId: data?.sshKeyId || "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (formData: Schema) => {
await mutateAsync({
name: formData.name,
description: formData.description || "",
ipAddress: formData.ipAddress || "",
port: formData.port || 22,
username: formData.username || "root",
sshKeyId: formData.sshKeyId || "",
serverId: serverId,
})
.then(async (data) => {
await utils.server.all.invalidate();
toast.success("Server Updated");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update a server");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Edit Server
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-3xl ">
<DialogHeader>
<DialogTitle>Update Server</DialogTitle>
<DialogDescription>
Update a server to deploy your applications remotely.
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-server"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4 ">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Hostinger Server" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="This server is for databases..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="sshKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a SSH Key</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a SSH Key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectLabel>
Registries ({sshKeys?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="ipAddress"
render={({ field }) => (
<FormItem>
<FormLabel>IP Address</FormLabel>
<FormControl>
<Input placeholder="192.168.1.100" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input placeholder="22" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="root" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
<DialogFooter>
<Button
isLoading={form.formState.isSubmitting}
form="hook-form-update-server"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -41,8 +41,8 @@ export const ShowUsers = () => {
}, []);
return (
<div className="h-full col-span-2">
<Card className="bg-transparent h-full ">
<div className=" col-span-2">
<Card className="bg-transparent ">
<CardHeader className="flex flex-row gap-2 justify-between w-full flex-wrap">
<div className="flex flex-col gap-2">
<CardTitle className="text-xl">Users</CardTitle>
@@ -55,9 +55,9 @@ export const ShowUsers = () => {
</div>
)}
</CardHeader>
<CardContent className="space-y-2 h-full">
<CardContent className="space-y-2">
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3">
<div className="flex flex-col items-center gap-3 h-full">
<Users className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground">
To create a user, you need to add:

View File

@@ -1,4 +1,3 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -6,334 +5,34 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { toast } from "sonner";
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 { ShowModalLogs } from "./web-server/show-modal-logs";
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
import { TerminalModal } from "./web-server/terminal-modal";
import React from "react";
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup";
import { UpdateServer } from "./web-server/update-server";
export const WebServer = () => {
const { data, refetch } = api.admin.one.useQuery();
const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation();
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
api.settings.toggleDashboard.useMutation();
const {
mutateAsync: cleanDockerBuilder,
isLoading: cleanDockerBuilderIsLoading,
} = api.settings.cleanDockerBuilder.useMutation();
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
api.settings.cleanMonitoring.useMutation();
const {
mutateAsync: cleanUnusedImages,
isLoading: cleanUnusedImagesIsLoading,
} = api.settings.cleanUnusedImages.useMutation();
const {
mutateAsync: cleanUnusedVolumes,
isLoading: cleanUnusedVolumesIsLoading,
} = api.settings.cleanUnusedVolumes.useMutation();
const {
mutateAsync: cleanStoppedContainers,
isLoading: cleanStoppedContainersIsLoading,
} = api.settings.cleanStoppedContainers.useMutation();
interface Props {
className?: string;
}
export const WebServer = ({ className }: Props) => {
const { data } = api.admin.one.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { mutateAsync: updateDockerCleanup } =
api.settings.updateDockerCleanup.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery();
return (
<Card className="rounded-lg w-full bg-transparent">
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
<CardHeader>
<CardTitle className="text-xl">Web server settings</CardTitle>
<CardDescription>Reload or clean the web server.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<CardContent className="flex flex-col gap-4 ">
<div className="grid md:grid-cols-2 gap-4">
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}>
<Button isLoading={isLoading} variant="outline">
Server
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadServer()
.then(async () => {
toast.success("Server Reloaded");
})
.catch(() => {
toast.success("Server Reloaded");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy">
<span>Watch logs</span>
</ShowModalLogs>
<ShowServerTraefikConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowServerTraefikConfig>
<ShowServerMiddlewareConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View middlewares config</span>
</DropdownMenuItem>
</ShowServerMiddlewareConfig>
<TerminalModal>
<span>Enter the terminal</span>
</TerminalModal>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
>
<Button
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
variant="outline"
>
Traefik
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadTraefik()
.then(async () => {
toast.success("Traefik Reloaded");
})
.catch(() => {
toast.error("Error to reload the traefik");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy-traefik">
<span>Watch logs</span>
</ShowModalLogs>
<ShowMainTraefikConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowMainTraefikConfig>
<EditTraefikEnv>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>Modify Env</span>
</DropdownMenuItem>
</EditTraefikEnv>
<DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard: !haveTraefikDashboardPortEnabled,
})
.then(async () => {
toast.success(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
refetchDashboard();
})
.catch(() => {
toast.error(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
});
}}
className="w-full cursor-pointer space-x-3"
>
<span>
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
Dashboard
</span>
</DropdownMenuItem>
<DockerTerminalModal appName="dokploy-traefik">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<span>Enter the terminal</span>
</DropdownMenuItem>
</DockerTerminalModal>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
>
<Button
isLoading={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
variant="outline"
>
Space
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedImages()
.then(async () => {
toast.success("Cleaned images");
})
.catch(() => {
toast.error("Error to clean images");
});
}}
>
<span>Clean unused images</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedVolumes()
.then(async () => {
toast.success("Cleaned volumes");
})
.catch(() => {
toast.error("Error to clean volumes");
});
}}
>
<span>Clean unused volumes</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanStoppedContainers()
.then(async () => {
toast.success("Stopped containers cleaned");
})
.catch(() => {
toast.error("Error to clean stopped containers");
});
}}
>
<span>Clean stopped containers</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanDockerBuilder()
.then(async () => {
toast.success("Cleaned Docker Builder");
})
.catch(() => {
toast.error("Error to clean Docker Builder");
});
}}
>
<span>Clean Docker Builder & System</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanMonitoring()
.then(async () => {
toast.success("Cleaned Monitoring");
})
.catch(() => {
toast.error("Error to clean Monitoring");
});
}}
>
<span>Clean Monitoring </span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanAll()
.then(async () => {
toast.success("Cleaned all");
})
.catch(() => {
toast.error("Error to clean all");
});
}}
>
<span>Clean all</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<ShowDokployActions />
<ShowTraefikActions />
<ShowStorageActions />
<UpdateServer />
</div>
@@ -345,25 +44,8 @@ export const WebServer = () => {
<span className="text-sm text-muted-foreground">
Version: {dokployVersion}
</span>
<div className="flex items-center gap-4">
<Switch
checked={data?.enableDockerCleanup}
onCheckedChange={async (e) => {
await updateDockerCleanup({
enableDockerCleanup: e,
})
.then(async () => {
toast.success("Docker Cleanup Enabled");
})
.catch(() => {
toast.error("Docker Cleanup Error");
});
refetch();
}}
/>
<Label className="text-primary">Daily Docker Cleanup</Label>
</div>
<ToggleDockerCleanup />
</div>
</CardContent>
</Card>

View File

@@ -17,6 +17,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import type React from "react";
import { useEffect, useState } from "react";
@@ -34,12 +35,14 @@ const Terminal = dynamic(
interface Props {
appName: string;
children?: React.ReactNode;
serverId?: string;
}
export const DockerTerminalModal = ({ children, appName }: Props) => {
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
@@ -65,7 +68,14 @@ export const DockerTerminalModal = ({ children, appName }: Props) => {
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
<SelectValue placeholder="Select a container" />
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -82,6 +92,7 @@ export const DockerTerminalModal = ({ children, appName }: Props) => {
</SelectContent>
</Select>
<Terminal
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
/>

View File

@@ -33,12 +33,15 @@ type Schema = z.infer<typeof schema>;
interface Props {
children?: React.ReactNode;
serverId?: string;
}
export const EditTraefikEnv = ({ children }: Props) => {
export const EditTraefikEnv = ({ children, serverId }: Props) => {
const [canEdit, setCanEdit] = useState(true);
const { data } = api.settings.readTraefikEnv.useQuery();
const { data } = api.settings.readTraefikEnv.useQuery({
serverId,
});
const { mutateAsync, isLoading, error, isError } =
api.settings.writeTraefikEnv.useMutation();
@@ -62,6 +65,7 @@ export const EditTraefikEnv = ({ children }: Props) => {
const onSubmit = async (data: Schema) => {
await mutateAsync({
env: data.env,
serverId,
})
.then(async () => {
toast.success("Traefik Env Updated");

View File

@@ -1,165 +0,0 @@
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";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
const UpdateMainTraefikConfigSchema = z.object({
traefikConfig: z.string(),
});
type UpdateTraefikConfig = z.infer<typeof UpdateMainTraefikConfigSchema>;
interface Props {
children?: React.ReactNode;
}
export const ShowMainTraefikConfig = ({ children }: Props) => {
const { data, refetch } = api.settings.readTraefikConfig.useQuery();
const [canEdit, setCanEdit] = useState(true);
const { mutateAsync, isLoading, error, isError } =
api.settings.updateTraefikConfig.useMutation();
const form = useForm<UpdateTraefikConfig>({
defaultValues: {
traefikConfig: "",
},
disabled: canEdit,
resolver: zodResolver(UpdateMainTraefikConfigSchema),
});
useEffect(() => {
if (data) {
form.reset({
traefikConfig: data || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateTraefikConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: error || "Invalid YAML",
});
return;
}
form.clearErrors("traefikConfig");
await mutateAsync({
traefikConfig: data.traefikConfig,
})
.then(async () => {
toast.success("Traefik config Updated");
refetch();
})
.catch(() => {
toast.error("Error to update the traefik config");
});
};
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Update traefik config</DialogTitle>
<DialogDescription>Update the traefik config</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-main-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full space-y-4 relative"
>
<div className="flex flex-col">
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`providers:
docker:
defaultRule: 'Host('dokploy.com')'
file:
directory: /etc/dokploy/traefik
watch: true
entryPoints:
web:
address: ':80'
websecure:
address: ':443'
api:
insecure: true
`}
{...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}
form="hook-form-update-main-traefik-config"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -18,6 +18,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import type React from "react";
import { useEffect, useState } from "react";
@@ -35,12 +36,14 @@ export const DockerLogsId = dynamic(
interface Props {
appName: string;
children?: React.ReactNode;
serverId?: string;
}
export const ShowModalLogs = ({ appName, children }: Props) => {
const { data } = api.docker.getContainersByAppLabel.useQuery(
export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
@@ -72,7 +75,14 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
<SelectValue placeholder="Select a container" />
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
@@ -88,7 +98,11 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
</SelectGroup>
</SelectContent>
</Select>
<DockerLogsId id="terminal" containerId={containerId || ""} />
<DockerLogsId
id="terminal"
containerId={containerId || ""}
serverId={serverId}
/>
</div>
</DialogContent>
</Dialog>

View File

@@ -1,162 +0,0 @@
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";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
const UpdateServerMiddlewareConfigSchema = z.object({
traefikConfig: z.string(),
});
type UpdateServerMiddlewareConfig = z.infer<
typeof UpdateServerMiddlewareConfigSchema
>;
interface Props {
children?: React.ReactNode;
}
export const ShowServerMiddlewareConfig = ({ children }: Props) => {
const { data, refetch } = api.settings.readMiddlewareTraefikConfig.useQuery();
const [canEdit, setCanEdit] = useState(true);
const { mutateAsync, isLoading, error, isError } =
api.settings.updateMiddlewareTraefikConfig.useMutation();
const form = useForm<UpdateServerMiddlewareConfig>({
defaultValues: {
traefikConfig: "",
},
disabled: canEdit,
resolver: zodResolver(UpdateServerMiddlewareConfigSchema),
});
useEffect(() => {
if (data) {
form.reset({
traefikConfig: data || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
console.log(error);
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: error || "Invalid YAML",
});
return;
}
form.clearErrors("traefikConfig");
await mutateAsync({
traefikConfig: data.traefikConfig,
})
.then(async () => {
toast.success("Middleware config Updated");
refetch();
})
.catch(() => {
toast.error("Error to update the middleware traefik config");
});
};
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Update Middleware config</DialogTitle>
<DialogDescription>Update the middleware config</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="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:
router-name:
rule: Host('domain.com')
service: container-name
entryPoints:
- web
tls: false
middlewares: []
`}
{...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}
form="hook-form-update-server-traefik-config"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,163 +0,0 @@
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";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
const UpdateServerTraefikConfigSchema = z.object({
traefikConfig: z.string(),
});
type UpdateServerTraefikConfig = z.infer<
typeof UpdateServerTraefikConfigSchema
>;
interface Props {
children?: React.ReactNode;
}
export const ShowServerTraefikConfig = ({ children }: Props) => {
const { data, refetch } = api.settings.readWebServerTraefikConfig.useQuery();
const [canEdit, setCanEdit] = useState(true);
const { mutateAsync, isLoading, error, isError } =
api.settings.updateWebServerTraefikConfig.useMutation();
const form = useForm<UpdateServerTraefikConfig>({
defaultValues: {
traefikConfig: "",
},
disabled: canEdit,
resolver: zodResolver(UpdateServerTraefikConfigSchema),
});
useEffect(() => {
if (data) {
form.reset({
traefikConfig: data || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateServerTraefikConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
console.log(error);
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: error || "Invalid YAML",
});
return;
}
form.clearErrors("traefikConfig");
await mutateAsync({
traefikConfig: data.traefikConfig,
})
.then(async () => {
toast.success("Traefik config Updated");
refetch();
})
.catch(() => {
toast.error("Error to update the traefik config");
});
};
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Update traefik config</DialogTitle>
<DialogDescription>Update the traefik config</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="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:
router-name:
rule: Host('domain.com')
service: container-name
entryPoints:
- web
tls: false
middlewares: []
`}
{...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}
form="hook-form-update-server-traefik-config"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,4 +1,3 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -8,79 +7,27 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import dynamic from "next/dynamic";
import type React from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { RemoveSSHPrivateKey } from "./remove-ssh-private-key";
const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), {
ssr: false,
});
const addSSHPrivateKey = z.object({
sshPrivateKey: z
.string({
required_error: "SSH private key is required",
})
.min(1, "SSH private key is required"),
});
type AddSSHPrivateKey = z.infer<typeof addSSHPrivateKey>;
interface Props {
children?: React.ReactNode;
serverId: string;
}
export const TerminalModal = ({ children }: Props) => {
const { data, refetch } = api.admin.one.useQuery();
const [user, setUser] = useState("root");
const [terminalUser, setTerminalUser] = useState("root");
const { mutateAsync, isLoading } =
api.settings.saveSSHPrivateKey.useMutation();
const form = useForm<AddSSHPrivateKey>({
defaultValues: {
sshPrivateKey: "",
export const TerminalModal = ({ children, serverId }: Props) => {
const { data } = api.server.one.useQuery(
{
serverId,
},
resolver: zodResolver(addSSHPrivateKey),
});
{ enabled: !!serverId },
);
useEffect(() => {
if (data) {
form.reset({});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: AddSSHPrivateKey) => {
await mutateAsync({
sshPrivateKey: formData.sshPrivateKey,
})
.then(async () => {
toast.success("SSH Key Updated");
await refetch();
})
.catch(() => {
toast.error("Error to Update the ssh key");
});
};
return (
<Dialog>
<DialogTrigger asChild>
@@ -92,75 +39,14 @@ export const TerminalModal = ({ children }: Props) => {
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogHeader className="flex flex-row justify-between pt-4">
<div>
<DialogTitle>Terminal</DialogTitle>
<DialogDescription>Easy way to access the server</DialogDescription>
</div>
{data?.haveSSH && (
<div>
<RemoveSSHPrivateKey />
</div>
)}
<DialogHeader className="flex flex-col gap-1">
<DialogTitle>Terminal ({data?.name})</DialogTitle>
<DialogDescription>Easy way to access the server</DialogDescription>
</DialogHeader>
{!data?.haveSSH ? (
<div>
<div className="flex flex-col gap-4">
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="grid w-full">
<FormField
control={form.control}
name="sshPrivateKey"
render={({ field }) => {
return (
<FormItem>
<FormLabel>SSH Private Key</FormLabel>
<FormDescription>
In order to access the server you need to add an
ssh private key
</FormDescription>
<FormControl>
<Textarea
placeholder={
"-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----"
}
className="h-32"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save
</Button>
</div>
</form>
</Form>
</div>
</div>
) : (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label>Log in as</Label>
<div className="flex flex-row gap-4">
<Input value={user} onChange={(e) => setUser(e.target.value)} />
<Button onClick={() => setTerminalUser(user)}>Login</Button>
</div>
</div>
<Terminal id="terminal" userSSH={terminalUser} />
</div>
)}
<div className="flex flex-col gap-4">
<Terminal id="terminal" serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);

View File

@@ -7,10 +7,10 @@ import { AttachAddon } from "@xterm/addon-attach";
interface Props {
id: string;
userSSH?: string;
serverId: string;
}
export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
export const Terminal: React.FC<Props> = ({ id, serverId }) => {
const termRef = useRef(null);
useEffect(() => {
@@ -33,7 +33,7 @@ export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/terminal?userSSH=${userSSH}`;
const wsUrl = `${protocol}//${window.location.host}/terminal?serverId=${serverId}`;
const ws = new WebSocket(wsUrl);
const addonAttach = new AttachAddon(ws);
@@ -46,7 +46,7 @@ export const Terminal: React.FC<Props> = ({ id, userSSH = "root" }) => {
return () => {
ws.readyState === WebSocket.OPEN && ws.close();
};
}, [id, userSSH]);
}, [id, serverId]);
return (
<div className="flex flex-col gap-4">

View File

@@ -74,7 +74,7 @@ export const SettingsLayout = ({ children }: Props) => {
{
title: "Cluster",
label: "",
icon: Server,
icon: BoxesIcon,
href: "/dashboard/settings/cluster",
},
{
@@ -83,6 +83,12 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Bell,
href: "/dashboard/settings/notifications",
},
{
title: "Servers",
label: "",
icon: Server,
href: "/dashboard/settings/servers",
},
]
: []),
...(user?.canAccessToSSHKeys
@@ -117,6 +123,7 @@ export const SettingsLayout = ({ children }: Props) => {
import {
Activity,
Bell,
BoxesIcon,
Database,
GitBranch,
KeyIcon,

View File

@@ -11,10 +11,11 @@ import {
} from "@/components/ui/alert-dialog";
interface Props {
title?: string;
description?: string;
title?: string | React.ReactNode;
description?: string | React.ReactNode;
onClick: () => void;
children?: React.ReactNode;
disabled?: boolean;
}
export const DialogAction = ({
@@ -22,6 +23,7 @@ export const DialogAction = ({
children,
description,
title,
disabled,
}: Props) => {
return (
<AlertDialog>
@@ -37,7 +39,9 @@ export const DialogAction = ({
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onClick}>Confirm</AlertDialogAction>
<AlertDialogAction disabled={disabled} onClick={onClick}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -1,35 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { HeartIcon } from "lucide-react";
export const ShowSupport = () => {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" className="rounded-full">
<span className="text-sm font-semibold">Support </span>
<HeartIcon className="size-4 text-red-500 fill-red-600 animate-heartbeat " />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl ">
<DialogHeader className="text-center flex justify-center items-center">
<DialogTitle>Dokploy Support</DialogTitle>
<DialogDescription>Consider supporting Dokploy</DialogDescription>
</DialogHeader>
<div className="grid w-full gap-4">
<div className="flex flex-col gap-4">
<span className="text-sm font-semibold">Name</span>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,81 @@
CREATE TABLE IF NOT EXISTS "server" (
"serverId" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"description" text,
"ipAddress" text NOT NULL,
"port" integer NOT NULL,
"username" text DEFAULT 'root' NOT NULL,
"appName" text NOT NULL,
"enableDockerCleanup" boolean DEFAULT false NOT NULL,
"createdAt" text NOT NULL,
"adminId" text NOT NULL,
"sshKeyId" text
);
--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "serverId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "server" ADD CONSTRAINT "server_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "server" ADD CONSTRAINT "server_sshKeyId_ssh-key_sshKeyId_fk" FOREIGN KEY ("sshKeyId") REFERENCES "public"."ssh-key"("sshKeyId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "application" ADD CONSTRAINT "application_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "postgres" ADD CONSTRAINT "postgres_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mariadb" ADD CONSTRAINT "mariadb_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mongo" ADD CONSTRAINT "mongo_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mysql" ADD CONSTRAINT "mysql_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "redis" ADD CONSTRAINT "redis_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "compose" ADD CONSTRAINT "compose_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -260,6 +260,13 @@
"when": 1725519351871,
"tag": "0036_tired_ronan",
"breakpoints": true
},
{
"idx": 37,
"version": "6",
"when": 1726988289562,
"tag": "0037_legal_namor",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.8.3",
"version": "v0.9.1",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -35,7 +35,6 @@
},
"dependencies": {
"rotating-file-stream": "3.2.3",
"@aws-sdk/client-s3": "3.515.0",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
@@ -130,7 +129,8 @@
"zod": "^3.23.4",
"zod-form-data": "^2.0.2",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0"
"@radix-ui/react-use-controllable-state": "1.1.0",
"ssh2": "1.15.0"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
@@ -167,7 +167,8 @@
"typescript": "^5.4.2",
"vite-tsconfig-paths": "4.3.2",
"vitest": "^1.6.0",
"xterm-readline": "1.1.1"
"xterm-readline": "1.1.1",
"@types/ssh2": "1.15.1"
},
"ct3aMetadata": {
"initVersion": "7.25.2"

View File

@@ -87,6 +87,7 @@ export default async function handler(
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
server: !!application.serverId,
};
await myQueue.add(
"deployments",

View File

@@ -63,6 +63,7 @@ export default async function handler(
type: "deploy",
applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`,
server: !!composeResult.serverId,
};
await myQueue.add(
"deployments",

View File

@@ -86,6 +86,7 @@ export default async function handler(
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
server: !!app.serverId,
};
await myQueue.add(
"deployments",

View File

@@ -0,0 +1,18 @@
import type { NextRequest } from "next/server";
import { renderToString } from "react-dom/server";
import Page418 from "../hola"; // Importa la página 418
export const GET = async (req: NextRequest) => {
// Renderiza el componente de la página 418 como HTML
const htmlContent = renderToString(Page418());
// Devuelve la respuesta con el código de estado HTTP 418
return new Response(htmlContent, {
headers: {
"Content-Type": "text/html",
},
status: 418,
});
};
export default GET;

View File

@@ -16,12 +16,14 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -98,6 +100,9 @@ const Service = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -125,10 +130,17 @@ const Service = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
@@ -152,14 +164,20 @@ const Service = (
<ShowEnvironment applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs appName={data?.appName || ""} />
<ShowDockerLogs
appName={data?.appName || ""}
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
<TabsContent value="deployments" className="w-full">

View File

@@ -10,12 +10,14 @@ import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -92,13 +94,16 @@ const Service = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
</p>
)}
</div>
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.composeStatus} />
@@ -119,10 +124,23 @@ const Service = (
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-y-scroll justify-start">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
data?.serverId && data?.composeType === "stack"
? "md:grid-cols-5"
: "",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
{data?.composeType === "docker-compose" && (
<TabsTrigger value="environment">Environment</TabsTrigger>
)}
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
@@ -147,19 +165,22 @@ const Service = (
<ShowEnvironmentCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogsCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>

View File

@@ -9,15 +9,16 @@ import { ShowInternalMariadbCredentials } from "@/components/dashboard/mariadb/g
import { UpdateMariadb } from "@/components/dashboard/mariadb/update-mariadb";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -81,7 +82,9 @@ const Mariadb = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
@@ -108,10 +111,17 @@ const Mariadb = (
}}
>
<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={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -136,14 +146,19 @@ const Mariadb = (
<ShowMariadbEnvironment mariadbId={mariadbId} />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs appName={data?.appName || ""} />
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">

View File

@@ -9,15 +9,16 @@ import { ShowInternalMongoCredentials } from "@/components/dashboard/mongo/gener
import { UpdateMongo } from "@/components/dashboard/mongo/update-mongo";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -82,7 +83,9 @@ const Mongo = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
@@ -109,10 +112,17 @@ const Mongo = (
}}
>
<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={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -138,14 +148,19 @@ const Mongo = (
<ShowMongoEnvironment mongoId={mongoId} />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs appName={data?.appName || ""} />
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">

View File

@@ -9,15 +9,16 @@ import { ShowGeneralMysql } from "@/components/dashboard/mysql/general/show-gene
import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-internal-mysql-credentials";
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
@@ -80,7 +81,9 @@ const MySql = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
@@ -108,10 +111,17 @@ const MySql = (
}}
>
<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={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
@@ -137,14 +147,19 @@ const MySql = (
<ShowMysqlEnvironment mysqlId={mysqlId} />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs appName={data?.appName || ""} />
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">

Some files were not shown because too many files have changed in this diff Show More