Compare commits

...

263 Commits

Author SHA1 Message Date
Mauricio Siu
a93f18eb4a Merge pull request #599 from Dokploy/canary
v0.10.4
2024-10-25 21:35:15 -06:00
Mauricio Siu
77e9617770 chore(version): bump version 2024-10-25 21:18:04 -06:00
Mauricio Siu
21e97b0175 Merge pull request #598 from Dokploy/593-use-advertise_addr-from-environment-variable-in-dokpoy-service
feat(dokploy): add env for ADVERTISE_ADDR to installation #593
2024-10-25 20:50:58 -06:00
Mauricio Siu
a6618a14d5 feat(dokploy): add env for ADVERTISE_ADDR to installation #593 2024-10-25 20:48:46 -06:00
Mauricio Siu
59308ab013 Merge pull request #596 from Dokploy/595-dokploy-internal-postgres-database-is-publicly-accessible-by-default
fix(dokploy): remove expose ports in production
2024-10-25 20:19:14 -06:00
Mauricio Siu
e19c8d7a7a fix(dokploy): remove expose ports in production 2024-10-25 17:09:06 -06:00
Mauricio Siu
421c93795b refactor(dokploy): prevent start worker when is cloud 2024-10-25 16:56:32 -06:00
Mauricio Siu
182f908c31 chore: add new sponsor 2024-10-25 10:20:56 -06:00
Mauricio Siu
20616363e9 refactor: update badge server 2024-10-25 00:39:34 -06:00
Mauricio Siu
d85073b26d Merge pull request #591 from Dokploy/feat/remove-build-on-server
Feat/remove build on server
2024-10-25 00:23:56 -06:00
Mauricio Siu
303d1b1b87 chore: add missing command 2024-10-25 00:21:12 -06:00
Mauricio Siu
60d4e1ba63 chore: update dockerfiles 2024-10-25 00:17:10 -06:00
Mauricio Siu
83d52b68f0 refactor: update 2024-10-25 00:05:55 -06:00
Mauricio Siu
af3b1a27f4 refactor: update tests 2024-10-24 23:57:40 -06:00
Mauricio Siu
7f94593c07 chore: revert ci/cd 2024-10-24 23:35:21 -06:00
Mauricio Siu
5df7654873 chore: update imports 2024-10-24 23:33:15 -06:00
Mauricio Siu
054836fd4c chore: update workflows 2024-10-24 23:28:52 -06:00
Mauricio Siu
484ead1f1f chore: update workflows 2024-10-24 23:26:06 -06:00
Mauricio Siu
fbada4c5de chore: set right filter pnpm dockerfile 2024-10-24 23:22:23 -06:00
Mauricio Siu
491113416b chore: remove server build 2024-10-24 23:20:51 -06:00
Mauricio Siu
c42f5cb799 refactor: update 2024-10-24 23:18:23 -06:00
Mauricio Siu
47aa223f87 refactor: remove save on build on next app and integrate turbopack 2024-10-24 23:13:24 -06:00
Mauricio Siu
cb586c9b74 Merge branch 'canary' into feat/payments 2024-10-24 13:50:17 -06:00
Mauricio Siu
554ac59b97 Merge pull request #589 from alok-debnath/typo-fixes
Typo fixes in README.md
2024-10-24 13:43:28 -06:00
Alok Debnath
0247898876 minor typo fixes in README.md for better readability 2024-10-25 00:06:09 +05:30
Mauricio Siu
467acc4d4d Merge pull request #586 from Dokploy/canary
v0.10.3
2024-10-24 01:21:17 -06:00
Mauricio Siu
fc2778db35 Merge pull request #585 from Dokploy/505-mongodb-backup-is-empty-in-output-pipeline
fix(dokploy): add missing --archive to mongodump
2024-10-24 01:14:10 -06:00
Mauricio Siu
85d6ff9012 chore(version): bump version 2024-10-24 01:11:43 -06:00
Mauricio Siu
fa053b4d1f refactor(dokploy): remove stripe from global scope 2024-10-24 01:10:17 -06:00
Mauricio Siu
522f8baec7 fix(dokploy): add missing --archive to mongodump 2024-10-24 01:07:02 -06:00
Mauricio Siu
bcc7afa3e4 refactor(dokploy): fix ts errors 2024-10-24 00:55:59 -06:00
Mauricio Siu
647a5d05a6 test(dokploy): add missing fields 2024-10-24 00:55:03 -06:00
Mauricio Siu
e15d41f80d chore: add radix tabs 2024-10-24 00:22:57 -06:00
Mauricio Siu
6c7c919d49 refactor: set servers quantity in 0 when the subscription is created 2024-10-24 00:21:28 -06:00
Mauricio Siu
22eb965919 Merge pull request #584 from dmbr0/patch-2
Fix typo in README.md
2024-10-23 20:08:37 -06:00
Alex Whitney
c0746b95b3 Fix typo in README.md 2024-10-23 20:18:59 -04:00
Mauricio Siu
dfdedf9e48 chore(dokploy): simplify migrations 2024-10-23 01:16:42 -06:00
Mauricio Siu
7e76eb4dd1 refactor: delete log 2024-10-23 01:06:34 -06:00
Mauricio Siu
c1f777e23e refactor: remove serverIp 2024-10-23 01:01:04 -06:00
Mauricio Siu
975d13c7e1 refactor(dokploy): disable stats monitoring 2024-10-23 00:58:38 -06:00
Mauricio Siu
017bdd2778 refactor(dokploy): add flag to prevent run commands when is cloud 2024-10-23 00:54:40 -06:00
Mauricio Siu
01e5cf0852 Merge pull request #580 from Dokploy/canary
v0.10.2
2024-10-22 21:17:38 -06:00
Mauricio Siu
548df8c0f4 Merge branch 'canary' into feat/payments 2024-10-22 20:31:18 -06:00
Mauricio Siu
8faa6ae1cf chore(version): bump version 2024-10-22 20:29:57 -06:00
Mauricio Siu
76ed1107c2 refactor(dokploy): add -r flag to read the enviroments vars 2024-10-22 20:19:49 -06:00
Mauricio Siu
cff5049096 Merge pull request #579 from Dokploy/578-unable-to-reset-my-password
fix(dokploy): use the exact path of functions #578
2024-10-22 20:02:43 -06:00
Mauricio Siu
cb5ca100a6 fix(dokploy): use the exact path of functions #578 2024-10-22 20:00:29 -06:00
Mauricio Siu
03d1e974dd Update LICENSE.MD 2024-10-22 16:53:12 -06:00
Mauricio Siu
1e6dbb5e8e feat(dokploy): add reset password for cloud 2024-10-22 01:25:13 -06:00
Mauricio Siu
431dadb6c2 feat: add pricing 2024-10-22 00:29:17 -06:00
Mauricio Siu
22e42b62ad feat: add is cloud in ssr 2024-10-21 22:38:23 -06:00
Mauricio Siu
49ee8ce132 Merge branch 'canary' into feat/payments 2024-10-21 22:22:59 -06:00
Mauricio Siu
b609d72d1c Merge pull request #576 from Dokploy/541-install-failed-due-to-docker-swarm-initialize-failed
fix(installation): exit of script when docker swarm init fails
2024-10-21 21:58:22 -06:00
Mauricio Siu
d7071fba60 fix(installation): exit of script when docker swarm init fails 2024-10-21 21:50:26 -06:00
Mauricio Siu
76585991ec Merge pull request #559 from eremannisto/fix/improve-faq-questions-and-answers
fix: Improve `FAQ` questions and answers
2024-10-21 21:31:00 -06:00
Mauricio Siu
da6efcf733 Merge pull request #406 from benbristow/fix/directus
fix: directus healthchecks (fix race condition starting up), volumes for uploads & extensions, add secret/db password generation, bump version to 11.0.2
2024-10-21 21:28:52 -06:00
Mauricio Siu
64b0770cfb Merge pull request #575 from Dokploy/574-clone-github-gitlab-and-bitbucket-submodules
feat(dokploy): add recurse submodules to providers #331
2024-10-21 21:25:18 -06:00
Mauricio Siu
1ec83a3236 feat(dokploy): add recurse submodules to providers #331 2024-10-21 21:14:40 -06:00
Mauricio Siu
9bace8e58b refactor: clean stripe customer if the customer is deleted 2024-10-21 13:01:08 -06:00
Mauricio Siu
c0afcaf3f6 refactor: show banner when the server is disabled 2024-10-21 02:19:10 -06:00
Mauricio Siu
53edf06476 refactor: remove comments 2024-10-21 00:48:04 -06:00
Mauricio Siu
255e9e4095 refactor: remove dokploy restart on notifications 2024-10-21 00:46:33 -06:00
Mauricio Siu
03f923c6e2 refactor: update show servers 2024-10-21 00:43:04 -06:00
Mauricio Siu
4685ef7439 refactor: update stripe envs 2024-10-21 00:39:14 -06:00
Mauricio Siu
626cfb80b4 refactor: add website url redirect 2024-10-21 00:37:11 -06:00
Mauricio Siu
9591fbff08 refactor: add Is cloud flag 2024-10-21 00:34:59 -06:00
Mauricio Siu
fbda00f059 refactor: update webhooks and added validation to prevent deploy when the server is inactive 2024-10-21 00:34:16 -06:00
Mauricio Siu
1907e7e59c feat: add webhook 2024-10-20 23:08:26 -06:00
Mauricio Siu
ffe7b04bea feat: add stripe webhooks 2024-10-20 15:08:44 -06:00
Mauricio Siu
fe0a662afd feat(cloud): add billing wip 2024-10-20 00:14:27 -06:00
Mauricio Siu
df9fad088f Merge pull request #567 from Dokploy/canary
v0.10.1
2024-10-18 23:12:12 -06:00
Mauricio Siu
319584d911 Merge branch 'canary' into feat/payments 2024-10-18 21:59:12 -06:00
Mauricio Siu
7d5a660f4d chore: bump version 2024-10-18 21:56:39 -06:00
Mauricio Siu
f7f0cbf318 Merge pull request #566 from arioberek/canary
fix: update reset password link URL
2024-10-18 09:34:51 -06:00
Arielton Oberek
841c0731aa fix: remove language prefix from reset password URL 2024-10-18 10:41:07 -03:00
Arielton Oberek
137cd25267 fix: reset password button URL 2024-10-18 10:35:08 -03:00
Mauricio Siu
2d40b2dfe5 feat: add stripe integration 2024-10-18 01:43:45 -06:00
Mauricio Siu
e32b713742 Merge branch 'canary' into feat/payments 2024-10-18 01:25:00 -06:00
Mauricio Siu
4dcd16c41e Merge pull request #564 from Dokploy/fix/git-ssh
fix(git): remove old references to ssh files to use the tmp file
2024-10-18 00:28:51 -06:00
Mauricio Siu
60497fe59d refactor: add husky 2024-10-18 00:10:01 -06:00
Mauricio Siu
8536945a60 styles: lint 2024-10-17 23:58:51 -06:00
Mauricio Siu
e0a8d8258c fix(git): remove old references to ssh files to use the tmp file 2024-10-17 23:55:27 -06:00
Mauricio Siu
988357fb55 refactor: update payments 2024-10-17 21:52:24 -06:00
Mauricio Siu
fe19cdb5e4 fix(setup): import directly from specific path 2024-10-16 15:57:51 -06:00
Mauricio Siu
c4654a9619 chore(contributing): update contributing 2024-10-16 13:23:25 -06:00
Ere Männistö
0a123a652b Improve FAQ questions and answers
- Improve questions and answers
- Fix typos
2024-10-15 19:54:41 +03:00
Mauricio Siu
5d437c29b2 refactor: remove header and navbar 2024-10-13 22:58:01 -06:00
Mauricio Siu
53f345ab1d feat: add privacy & terms 2024-10-13 22:54:02 -06:00
Mauricio Siu
2644b638d1 Merge pull request #553 from Dokploy/canary
v0.10.0
2024-10-13 16:30:00 -06:00
Mauricio Siu
8785282133 chore(version): bump version 2024-10-13 11:51:52 -06:00
Mauricio Siu
35c084af1d Merge pull request #550 from Dokploy/fix/env-parsing
fix(logs): improve logs in remote server when is error, and fix the e…
2024-10-13 11:01:28 -06:00
Mauricio Siu
b63488baba Merge pull request #547 from Dokploy/536-implement-custom-certificates-in-external-server
feat(certificates): create certificates in a remote server
2024-10-13 11:01:16 -06:00
Mauricio Siu
7e5f21b28e style: lint 2024-10-13 02:28:04 -06:00
Mauricio Siu
8d41bafb93 fix(logs): improve logs in remote server when is error, and fix the env parsing to allow any values in enviroment variables 2024-10-13 02:27:33 -06:00
Mauricio Siu
6f5049efd5 styles: lint 2024-10-12 19:33:56 -06:00
Mauricio Siu
6dd6b636e5 feat(certificates): create certificates in a remote server 2024-10-12 19:09:50 -06:00
Mauricio Siu
8488d530f3 Merge pull request #546 from Dokploy/542-cannot-save-server-address-without-lets-encrypt-email
fix(traefik): allow to save domain without letsencrypt email when the…
2024-10-12 16:58:50 -06:00
Mauricio Siu
6c61d5cdf5 Merge pull request #545 from mezotv/fix-sponsor-size
fix(website): improve sponsor logo alignment
2024-10-12 16:45:40 -06:00
Mauricio Siu
8036455c2d fix(traefik): allow to save domain without letsencrypt email when the cert is none #542 2024-10-12 16:44:10 -06:00
Dominik Koch
bf44eeab3d style: format code 2024-10-12 22:29:32 +00:00
Dominik Koch
0cd185696d fix: add target blank to links 2024-10-12 22:26:48 +00:00
Mauricio Siu
339697437a Merge pull request #544 from Dokploy/533-the-traefik-labels-get-duplicated-if-i-have-two-services-and-two-domains-in-one-docker-compose-file
fix(compose): refetch compose file when enter to the modal to prevent…
2024-10-12 16:26:43 -06:00
Dominik Koch
e0b15fe971 fix: logo alignment 2024-10-12 22:23:07 +00:00
Mauricio Siu
afc5ea43da fix(compose): refetch compose file when enter to the modal to prevent show duplicate labels 2024-10-12 16:19:49 -06:00
Mauricio Siu
bb337e819e Merge pull request #539 from mezotv/fix-typo
Fix typos and grammar errors
2024-10-12 16:11:27 -06:00
Mauricio Siu
160dd10f77 Merge pull request #538 from mezotv/update-website-sponsors
Add organization sponsors to website
2024-10-12 16:10:46 -06:00
Dominik Koch
49b096fef6 style: format code 2024-10-12 21:08:58 +00:00
Dominik Koch
4828b840cb fix: lxaer logo 2024-10-12 23:05:30 +02:00
Dominik Koch
67af70448b Update README.md 2024-10-12 23:03:09 +02:00
Dominik Koch
946acf5245 style: format code 2024-10-12 20:59:49 +00:00
Dominik Koch
390b3a835a fix: new lxaer logo 2024-10-12 22:54:46 +02:00
Dominik Koch
2842bf9a91 Update README.md 2024-10-12 22:51:56 +02:00
Dominik Koch
8a0e10f6f4 Update README.md 2024-10-12 22:51:03 +02:00
Mauricio Siu
67efa82b91 Merge pull request #537 from mezotv/organization-readme
Add missing organization to readme
2024-10-11 12:41:27 -06:00
Dominik Koch
546d6b87ea fix: get rid of typos and fix grammar 2024-10-11 20:03:20 +02:00
Dominik Koch
d012d19253 feat(web): add organization sponsors 2024-10-11 11:28:10 +02:00
Dominik Koch
e925ed9ea4 fix: add missing organization to readme 2024-10-11 11:21:20 +02:00
Mauricio Siu
0c05809d7d refactor: remove 2024-10-10 23:38:16 -06:00
Mauricio Siu
29f1631950 refactor: add dynamic import queue 2024-10-10 23:34:56 -06:00
Mauricio Siu
9c0c58035a chore; add logs for process env 2024-10-10 22:40:02 -06:00
Mauricio Siu
f4262569dd chore: add cat 2024-10-10 22:37:36 -06:00
Mauricio Siu
99cf6eae49 refactor: adjust enviroment variables 2024-10-10 22:27:25 -06:00
Mauricio Siu
0488546706 refactor: print env 2024-10-10 22:17:19 -06:00
Mauricio Siu
dc32cd71e5 refactor: add env to dist 2024-10-10 22:01:14 -06:00
Mauricio Siu
efdfd5d13c chore: print envs 2024-10-10 21:52:50 -06:00
Mauricio Siu
d31cab76f0 refactor: add flag 2024-10-10 21:52:09 -06:00
Mauricio Siu
00ed202127 refactor: add .env 2024-10-10 21:40:43 -06:00
Mauricio Siu
aaa4ca297d refacctor: add missing envs 2024-10-10 21:32:27 -06:00
Mauricio Siu
629871e683 refactor: add option to ci/cd 2024-10-10 21:22:13 -06:00
Mauricio Siu
a237c651c3 chore: add autoprefixer 2024-10-07 01:47:01 -06:00
Mauricio Siu
4f9b0d9d59 chore: remove .env 2024-10-07 01:45:16 -06:00
Mauricio Siu
b701a0b504 Merge pull request #531 from Dokploy/feat/cloud
Feat/cloud
2024-10-07 01:44:00 -06:00
Mauricio Siu
39036202bb test: fix mock zip drop 2024-10-07 01:34:35 -06:00
Mauricio Siu
4225ad83e7 chore: add docker images 2024-10-07 01:28:22 -06:00
Mauricio Siu
38c4e0ede1 chore: add build server 2024-10-07 01:16:59 -06:00
Mauricio Siu
25a64c703f chore: add pnpm run build 2024-10-07 01:08:47 -06:00
Mauricio Siu
ab5871add7 chore: biome 2024-10-07 01:05:47 -06:00
Mauricio Siu
2b0e009f6a refactor: remove feature tag 2024-10-07 00:59:07 -06:00
Mauricio Siu
9b6ea99eea refactor: remove unused files 2024-10-07 00:49:54 -06:00
Mauricio Siu
c4cf545d85 Merge branch 'canary' into feat/cloud 2024-10-07 00:31:23 -06:00
Mauricio Siu
1edd717432 Merge pull request #524 from lorenzomigliorero/feat/domains-link
feat: add dropdown link
2024-10-07 00:26:23 -06:00
Mauricio Siu
5b88af6158 refactor: remove findAdmin 2024-10-06 15:04:54 -06:00
Mauricio Siu
e995d894d8 refactor: remove docker from cloud 2024-10-06 14:27:14 -06:00
Mauricio Siu
5f56512e56 refactor: update queue jobs 2024-10-06 14:16:31 -06:00
Mauricio Siu
24e4930fc1 refactor: use connection IORedis 2024-10-06 02:57:46 -06:00
Mauricio Siu
541728805f refactor: add health path to middleware 2024-10-06 02:46:07 -06:00
Mauricio Siu
9e4bac1386 refactor: update ioredis connection 2024-10-06 02:44:35 -06:00
Mauricio Siu
ed8d32d050 chore: add type module 2024-10-06 02:25:02 -06:00
Mauricio Siu
7fb66bc58b refactor:add remote cron jobs 2024-10-06 02:19:15 -06:00
Mauricio Siu
58c06fba86 refactor(cloud): add api key for autentication between servers 2024-10-06 01:56:53 -06:00
Mauricio Siu
3cf27a068a refactor: add authorization 2024-10-06 01:37:39 -06:00
Mauricio Siu
7cfbea3f60 refactor: update 2024-10-05 22:18:33 -06:00
Mauricio Siu
7907e33431 refactor: update namefile 2024-10-05 22:17:44 -06:00
Mauricio Siu
89f3078ce5 refactor: update package name 2024-10-05 22:15:57 -06:00
Mauricio Siu
f3ce69b656 refactor: rename builders to server 2024-10-05 22:15:47 -06:00
Mauricio Siu
43555cdabe feat(schedules): add schedules server 2024-10-05 22:11:38 -06:00
Mauricio Siu
651bf3a303 refactor: update ref 2024-10-05 18:42:08 -06:00
Mauricio Siu
4cde1a8a7d refactor: close docker logs 2024-10-05 18:20:28 -06:00
Mauricio Siu
405efcac0b refactor: update logs listener 2024-10-05 17:34:17 -06:00
Mauricio Siu
8397de0dca refactor: close connections when the ws is not ready 2024-10-05 17:17:46 -06:00
Mauricio Siu
06b58e6495 refactor: close connection ws 2024-10-05 16:46:02 -06:00
Mauricio Siu
24db4006cf refactor: add close ws 2024-10-05 16:45:53 -06:00
Mauricio Siu
84009c5e9b refactor: add close websocket 2024-10-05 16:04:28 -06:00
Mauricio Siu
e32afde973 refactor: add status ok 2024-10-05 14:37:45 -06:00
Mauricio Siu
fecffac573 refactor: test rollback 2024-10-05 14:27:00 -06:00
Mauricio Siu
b4448e013c test: rollback 2024-10-05 14:16:23 -06:00
Mauricio Siu
c3f06a6272 feat(cloud): add healtchecks 2024-10-05 13:34:00 -06:00
Mauricio Siu
2be724f780 Update README.md 2024-10-05 02:39:50 -06:00
Mauricio Siu
bf78326c96 chore: use the same tailwindcss version 2024-10-05 01:30:09 -06:00
Mauricio Siu
4ca8722c6e refactor: optimize dockerfile 2024-10-05 01:21:39 -06:00
Mauricio Siu
e56e1eb687 refactor: add autoprefixer 2024-10-05 01:15:52 -06:00
Mauricio Siu
5a5c302bdc chore: optimize image 2024-10-05 01:11:15 -06:00
Mauricio Siu
997dc85985 refactor(api): remove deploy normal compose on the same dokploy server 2024-10-05 00:34:41 -06:00
Mauricio Siu
09ef851372 refactor(cloud): add validation to prevent create applications without server 2024-10-04 21:31:22 -06:00
Mauricio Siu
7c4987d84d refactor(cloud): add validation to prevent access to shared resources 2024-10-04 20:44:57 -06:00
Mauricio Siu
5cebf5540a refactor(cloud): add deploy to external API 2024-10-04 18:53:46 -06:00
Mauricio Siu
3df2f8e58c refactor(terminal): use ssh2 instead of cmd 2024-10-04 17:24:17 -06:00
Mauricio Siu
a642d36a23 refactor: hide handler when is cloud version 2024-10-04 15:20:47 -06:00
Mauricio Siu
72bceec62d refactor: add try catch 2024-10-04 15:16:01 -06:00
Mauricio Siu
3d32314e80 chore: update lock 2024-10-04 15:14:56 -06:00
Mauricio Siu
7259830ac1 chore: add experimental specifier resolution flag 2024-10-04 15:14:30 -06:00
Mauricio Siu
daa87c0dc7 refactor(cloud): add is cloud flag to cluters 2024-10-04 14:40:31 -06:00
Mauricio Siu
a2ee55e0e9 Merge pull request #527 from kikoncuo/fix/application-create-missing-return
fix: Application create endpoint returns value
2024-10-04 13:39:21 -06:00
Enrique
de6aeac243 fix(application.create): add missing return statement and align response with application.one 2024-10-04 11:46:14 +02:00
Lorenzo Migliorero
f640b4a87f stop propagation 2024-10-04 11:03:11 +02:00
Mauricio Siu
3747db08d4 refactor(cloud): add validation to prevent execute in cloud version 2024-10-04 01:12:14 -06:00
Mauricio Siu
ab4677ac0e refactor(auth): set null when the findAdmin is null 2024-10-04 01:01:30 -06:00
Mauricio Siu
172d55311e chore(cloud): add migrations 2024-10-04 00:04:52 -06:00
Mauricio Siu
3ce25e2ac8 chore(migrations): remove migrations 2024-10-03 23:47:34 -06:00
Mauricio Siu
388ded9aa5 feat(cloud): add deploy on remote worker 2024-10-03 23:46:26 -06:00
Mauricio Siu
767d3e1944 refactor(cloud): add validation to prevent access to shared resources 2024-10-03 19:50:17 -06:00
Mauricio Siu
ec1d6c7430 refactor(cloud): add validation to prevent access to resources from another admin 2024-10-03 19:48:49 -06:00
Mauricio Siu
8abeae5e63 refactor(cloud): validate all the routes to prevent get access from private resource 2024-10-03 19:34:38 -06:00
Mauricio Siu
cc90d9ec9b Merge branch 'canary' into feat/cloud 2024-10-03 13:39:06 -06:00
Mauricio Siu
6b5de00fb0 chore: update dev builder command 2024-10-03 12:01:32 -06:00
Mauricio Siu
5ed96fb0ce Merge pull request #523 from lorenzomigliorero/fix/watch-mode
fix: tsx watch flag unwanted reload
2024-10-03 11:01:57 -06:00
Mauricio Siu
a73af1d578 Merge pull request #522 from lorenzomigliorero/fix/bitbucket-repositories-length
fix: bitbucket repositories length
2024-10-03 11:00:25 -06:00
Mauricio Siu
5867a27901 Merge pull request #459 from seppulcro/feat/add-roundcube-template
feat: add roundcube template
2024-10-03 10:59:41 -06:00
Lorenzo Migliorero
8f7bffc349 add fragment 2024-10-03 15:32:45 +02:00
Lorenzo Migliorero
21ee22d4f5 add dropdown link 2024-10-03 15:27:48 +02:00
Lorenzo Migliorero
fb72132a4b remove watch flag 2024-10-03 14:05:12 +02:00
Lorenzo Migliorero
ca904c15d9 remove console.log 2024-10-03 13:55:28 +02:00
Lorenzo Migliorero
682863f83e fix: repo length 2024-10-03 13:54:40 +02:00
Mauricio Siu
acd722678e Merge pull request #521 from Dokploy/canary
v0.9.4
2024-10-03 02:12:03 -06:00
Mauricio Siu
3750977f41 Merge pull request #520 from Dokploy/487-private-docker-container-pull-failed-despite-having-docker-registry-configured-in-registry
fix(registry): add option to login the registry in the remote server
2024-10-03 02:02:51 -06:00
Mauricio Siu
9b401059b0 fix(registry): add option to login the registry in the remote server 2024-10-03 01:56:50 -06:00
Mauricio Siu
6a3ef5c860 Merge pull request #519 from Dokploy/514-failing-to-refresh-docker-composeyml-from-github-repo
fix(compose): delete content when is remote server
2024-10-03 01:32:50 -06:00
seppulcro
a36518a8f0 fix: set static docker image version 2024-10-03 08:09:21 +01:00
Mauricio Siu
a5eb4b0a72 fix(compose): delete content when is remote server 2024-10-03 01:00:35 -06:00
Mauricio Siu
b5c0876dd4 Merge pull request #518 from Dokploy/515-non-admin-users-are-not-able-to-set-up-database-backup
fix(destinations): change admin to protected procedure
2024-10-03 00:53:23 -06:00
Mauricio Siu
9745d12ac8 fix(destinations): change admin to protected procedure 2024-10-03 00:48:00 -06:00
Mauricio Siu
5c72e5a452 refactor: filter by adminId 2024-10-03 00:45:46 -06:00
Mauricio Siu
600f4b2106 refactor: apply migration 2024-10-03 00:15:36 -06:00
Mauricio Siu
c12d37fe0a refactor: revent 2024-10-03 00:06:11 -06:00
Mauricio Siu
bba8d00ba2 refactor: update dockerfile 2024-10-02 23:56:09 -06:00
Mauricio Siu
78665ffbfa refactor(tabs): hide when is cloud version 2024-10-02 23:51:37 -06:00
Mauricio Siu
d41c8c70c3 feat: add ssh key 2024-10-02 22:50:01 -06:00
Mauricio Siu
f13e5d449c Revert "refactor: stash"
This reverts commit d256998677.
2024-10-02 22:37:14 -06:00
Mauricio Siu
d256998677 refactor: stash 2024-10-02 21:55:54 -06:00
Mauricio Siu
4aaf04ce74 Merge pull request #506 from AprilNEA/fix/domin-port-number-convert
Fix port input value becoming NaN
2024-10-02 13:10:17 -06:00
Mauricio Siu
ecfca9419a refactor: remove innecessary conversion 2024-10-02 13:02:20 -06:00
AprilNEA
dfd6764320 styles: format code with prettier 2024-10-02 18:22:21 +00:00
Mauricio Siu
73bf5274f5 chore(version): bump version 2024-10-01 14:28:11 -06:00
AprilNEA
fc38a42587 fix: convert final value 2024-10-01 14:36:45 +00:00
Mauricio Siu
9b255964fe Merge pull request #511 from Dokploy/509-create-compose-modal-remains-open-after-clicking-create
509 create compose modal remains open after clicking create
2024-09-30 21:33:56 -06:00
Mauricio Siu
29f55ca1a0 Merge pull request #496 from missuo/canary
feat: add update option
2024-09-30 15:04:03 -06:00
Mauricio Siu
6a5fb8faff fix(multi-server): show the servers ip instead of the main ip #502 2024-09-30 15:00:32 -06:00
Mauricio Siu
5c225c8d42 fix(modal): close the modal after the creation #509 2024-09-30 15:00:01 -06:00
AprilNEA
c1c5fc978b fix: fix number convert when string empty 2024-09-30 08:35:49 +00:00
Mauricio Siu
ffd19f591d chore: add package manager 2024-09-30 01:00:53 -06:00
Mauricio Siu
81a41a7f31 refactor: update healtcheck 2024-09-30 00:54:49 -06:00
Mauricio Siu
1c9b704ecc refactor: update redis url 2024-09-30 00:51:07 -06:00
Mauricio Siu
edf1fdedf0 refactor: update paths build 2024-09-30 00:47:47 -06:00
Mauricio Siu
8484649071 refactor: update 2024-09-30 00:44:33 -06:00
Mauricio Siu
1e68248611 refactor: update image 2024-09-30 00:41:21 -06:00
Mauricio Siu
b3e35c5838 refactor: upate dockerfile 2024-09-30 00:40:43 -06:00
Mauricio Siu
ddd4ba8135 refactor: add dockerfile 2024-09-30 00:39:53 -06:00
Mauricio Siu
539544d0de refactor: update 2024-09-30 00:38:22 -06:00
Mauricio Siu
123b5d098b refactor: update install 2024-09-30 00:32:54 -06:00
Mauricio Siu
796a9ca11f refactor: add builder workspace 2024-09-30 00:31:56 -06:00
Mauricio Siu
2872ef3ccb feat(api): add dockerfile api 2024-09-30 00:27:14 -06:00
Mauricio Siu
06a772e344 chore: add dotenv 2024-09-29 22:55:41 -06:00
Mauricio Siu
e99666f4c0 fix(esm): add tsc alias 2024-09-29 21:43:25 -06:00
Mauricio Siu
bd243d79e2 refactor: remove logs 2024-09-29 21:01:24 -06:00
Vincent Yang
18b4b23f79 feat: add update option for canary and feature tag 2024-09-29 22:45:39 -04:00
Mauricio Siu
071a9d5104 refactor: cleanup dependencies 2024-09-29 20:35:25 -06:00
Mauricio Siu
61ebd1b16e refactor(server): remove files 2024-09-29 19:14:41 -06:00
Mauricio Siu
9836c988a0 refactor(build): update imports 2024-09-29 18:53:32 -06:00
Mauricio Siu
03d7738032 refactor(dockerfile): update dockerfile 2024-09-29 18:49:07 -06:00
Mauricio Siu
98aa474975 refactor(test): update paths and mocks 2024-09-29 18:44:07 -06:00
Mauricio Siu
7bd6b66551 refactor(multi-server): update path imports 2024-09-29 18:04:45 -06:00
Mauricio Siu
2ae7e562bb refactor(server): remove files 2024-09-29 13:28:24 -06:00
Mauricio Siu
e4b998c608 refactor(server): update imports 2024-09-29 11:55:29 -06:00
Mauricio Siu
9b7aacc934 refactor(server): split logic in to packages 2024-09-29 02:28:58 -06:00
Vincent Young
7027f39c48 feat: add update option 2024-09-28 15:06:20 -04:00
seppulcro
0aff344bc0 fix: change tags for roundcube template 2024-09-24 17:27:11 +01:00
seppulcro
4715f34e15 fix: mdx-components formatting with biome 2024-09-24 17:01:24 +01:00
seppulcro
59386ed4b7 fix: Update docs; Fix useMDXComponents for li override: add missing id for correct remark-gfm footnotes functionallity 2024-09-23 16:13:16 +01:00
seppulcro
5b0bf99cbf fix: update roundcube template remove bad chars 2024-09-19 14:40:20 +01:00
seppulcro
8e227a3286 fix: update roundcube template to match spec 2024-09-19 08:37:37 +01:00
seppulcro
869e58739f feat: add roundcube template 2024-09-17 19:57:54 +01:00
Ben Bristow
f8721d3e04 fix: remove 'networks' section 2024-09-02 21:20:27 +01:00
Ben Bristow
a6c7c3b031 fix: directus healthchecks (fix race condition starting up), volumes for uploads & extensions, add secret/db password generation, bump version to 11.0.2 2024-09-02 21:11:03 +01:00
431 changed files with 27956 additions and 4254 deletions

View File

@@ -11,6 +11,7 @@ jobs:
command: | command: |
cp apps/dokploy/.env.production.example .env.production cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- run: - run:
name: Build and push AMD64 image name: Build and push AMD64 image
command: | command: |
@@ -61,7 +62,7 @@ jobs:
VERSION=$(node -p "require('./apps/dokploy/package.json').version") VERSION=$(node -p "require('./apps/dokploy/package.json').version")
echo $VERSION echo $VERSION
TAG="latest" TAG="latest"
docker manifest create dokploy/dokploy:${TAG} \ docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \ dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64 dokploy/dokploy:${TAG}-arm64

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -1,4 +1,4 @@
name: Build Docs & Website Docker images name: Build Docker images
on: on:
push: push:
@@ -48,3 +48,74 @@ jobs:
push: true push: true
tags: dokploy/website:latest tags: dokploy/website:latest
platforms: linux/amd64 platforms: linux/amd64
build-and-push-cloud-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.cloud
push: true
tags: |
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64
build-and-push-schedule-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.schedule
push: true
tags: |
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64
build-and-push-server-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.server
push: true
tags: |
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
platforms: linux/amd64

View File

@@ -18,6 +18,7 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm biome ci - run: pnpm biome ci
- run: pnpm typecheck - run: pnpm typecheck
@@ -32,6 +33,7 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm build - run: pnpm build
parallel-tests: parallel-tests:
@@ -44,4 +46,5 @@ jobs:
node-version: 18.18.0 node-version: 18.18.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm test - run: pnpm test

1
.husky/commit-msg Normal file
View File

@@ -0,0 +1 @@
npx commitlint --edit "$1"

6
.husky/install.mjs Normal file
View File

@@ -0,0 +1,6 @@
// Skip Husky install in production and CI
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
process.exit(0);
}
const husky = (await import("husky")).default;
console.log(husky());

2
.husky/pre-commit Normal file
View File

@@ -0,0 +1,2 @@
pnpm run check
git add .

View File

@@ -71,6 +71,11 @@ Run the command that will spin up all the required services and files.
pnpm run dokploy:setup pnpm run dokploy:setup
``` ```
Run this script
```bash
pnpm run server:script
```
Now run the development server. Now run the development server.
```bash ```bash

View File

@@ -15,7 +15,10 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# Deploy only the dokploy app # Deploy only the dokploy app
ENV NODE_ENV=production ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server switch:prod
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build RUN pnpm --filter=./apps/dokploy run build
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next

53
Dockerfile.cloud Normal file
View File

@@ -0,0 +1,53 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server switch:prod
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
COPY --from=build /prod/dokploy/dist ./dist
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
COPY --from=build /prod/dokploy/public ./public
COPY --from=build /prod/dokploy/package.json ./package.json
COPY --from=build /prod/dokploy/drizzle ./drizzle
COPY --from=build /prod/dokploy/components.json ./components.json
COPY --from=build /prod/dokploy/node_modules ./node_modules
# Install RCLONE
RUN curl https://rclone.org/install.sh | bash
# tsx
RUN pnpm install -g tsx
EXPOSE 3000
CMD [ "pnpm", "start" ]

37
Dockerfile.schedule Normal file
View File

@@ -0,0 +1,37 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server switch:prod
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/schedules run build
RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
# Copy only the necessary files
COPY --from=build /prod/schedules/dist ./dist
COPY --from=build /prod/schedules/package.json ./package.json
COPY --from=build /prod/schedules/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start

37
Dockerfile.server Normal file
View File

@@ -0,0 +1,37 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
# Deploy only the dokploy app
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server switch:prod
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/api run build
RUN pnpm --filter=./apps/api --prod deploy /prod/api
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
FROM base AS dokploy
WORKDIR /app
# Set production
ENV NODE_ENV=production
# Copy only the necessary files
COPY --from=build /prod/api/dist ./dist
COPY --from=build /prod/api/package.json ./package.json
COPY --from=build /prod/api/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start

View File

@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
## Additional Terms for Specific Features ## Additional Terms for Specific Features
The following additional terms apply to the multi-node support 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. - **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 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. - **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 and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service. - **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. For further inquiries or permissions, please contact us directly.

View File

@@ -15,28 +15,29 @@
</div> </div>
</div> </div>
<br /> <br />
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases. Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features ### Features
Dokploy include multiples features to make your life easier. Dokploy includes multiple features to make your life easier.
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.). - **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis. - **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
- **Backups**: Automate backups for databases to a external storage destination. - **Backups**: Automate backups for databases to an external storage destination.
- **Docker Compose**: Native support for Docker Compose to manage complex applications. - **Docker Compose**: Native support for Docker Compose to manage complex applications.
- **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster. - **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
- **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.). - **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing. - **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource. - **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
- **Docker Management**: Easily deploy and manage Docker containers. - **Docker Management**: Easily deploy and manage Docker containers.
- **CLI/API**: Manage your applications and databases using the command line or trought the API. - **CLI/API**: Manage your applications and databases using the command line or through the API.
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.) - **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
- **Multi Server**: Deploy and manage your applications remotely to external servers.
- **Self-Hosted**: Self-host Dokploy on your VPS. - **Self-Hosted**: Self-host Dokploy on your VPS.
## 🚀 Getting Started ## 🚀 Getting Started
To get started run the following command in a VPS: To get started, run the following command on a VPS:
```bash ```bash
curl -sSL https://dokploy.com/install.sh | sh curl -sSL https://dokploy.com/install.sh | sh
@@ -58,7 +59,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Hero Sponsors 🎖 ### Hero Sponsors 🎖
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" ><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/></a> <div style="display: flex; align-items: center; gap: 20px;">
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
</a>
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
</a>
</div>
### Premium Supporters 🥇 ### Premium Supporters 🥇
@@ -81,6 +89,9 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<div style="display: flex; gap: 30px; flex-wrap: wrap;"> <div style="display: flex; gap: 30px; flex-wrap: wrap;">
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a> <a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
</div> </div>
#### Organizations: #### Organizations:

View File

@@ -1,15 +1,33 @@
{ {
"name": "my-app", "name": "@dokploy/api",
"version": "0.0.1",
"type": "module",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts" "dev": "PORT=4000 tsx watch src/index.ts",
"build": "tsc --project tsconfig.json",
"start": "node --experimental-specifier-resolution=node dist/index.js",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"@hono/zod-validator": "0.3.0",
"zod": "^3.23.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.12.1", "@hono/node-server": "^1.12.1",
"hono": "^4.5.8", "hono": "^4.5.8",
"dotenv": "^16.3.1" "dotenv": "^16.3.1",
"redis": "4.7.0",
"@nerimity/mimiqueue": "1.2.3"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.4.2",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"tsx": "^4.7.1" "tsx": "^4.7.1"
} },
"packageManager": "pnpm@9.5.0"
} }

View File

@@ -1,66 +1,61 @@
import { serve } from "@hono/node-server"; import { serve } from "@hono/node-server";
import { config } from "dotenv";
import { Hono } from "hono"; import { Hono } from "hono";
import { cors } from "hono/cors"; import "dotenv/config";
import { validateLemonSqueezyLicense } from "./utils"; import { zValidator } from "@hono/zod-validator";
import { Queue } from "@nerimity/mimiqueue";
config(); import { createClient } from "redis";
import { logger } from "./logger";
import { type DeployJob, deployJobSchema } from "./schema";
import { deploy } from "./utils";
const app = new Hono(); const app = new Hono();
const redisClient = createClient({
app.use( url: process.env.REDIS_URL,
"/*",
cors({
origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
maxAge: 600,
credentials: true,
}),
);
export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
app.get("/v1/health", (c) => {
return c.text("Hello Hono!");
}); });
app.post("/v1/validate-license", async (c) => { app.use(async (c, next) => {
const { licenseKey } = await c.req.json(); if (c.req.path === "/health") {
return next();
}
const authHeader = c.req.header("X-API-Key");
if (!licenseKey) { if (process.env.API_KEY !== authHeader) {
return c.json({ error: "License key is required" }, 400); return c.json({ message: "Invalid API Key" }, 403);
} }
try { return next();
const licenseValidation = await validateLemonSqueezyLicense(licenseKey);
if (licenseValidation.valid) {
return c.json({
valid: true,
message: "License is valid",
metadata: licenseValidation.meta,
});
}
return c.json(
{
valid: false,
message: licenseValidation.error || "Invalid license",
},
400,
);
} catch (error) {
console.error("Error during license validation:", error);
return c.json({ error: "Internal server error" }, 500);
}
}); });
const port = 4000; app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
console.log(`Server is running on port ${port}`); const data = c.req.valid("json");
const res = queue.add(data, { groupName: data.serverId });
serve({ return c.json(
fetch: app.fetch, {
port, message: "Deployment Added",
},
200,
);
}); });
app.get("/health", async (c) => {
return c.json({ status: "ok" });
});
const queue = new Queue({
name: "deployments",
process: async (job: DeployJob) => {
logger.info("Deploying job", job);
return await deploy(job);
},
redisClient,
});
(async () => {
await redisClient.connect();
await redisClient.flushAll();
logger.info("Redis Cleaned");
})();
const port = Number.parseInt(process.env.PORT || "3000");
logger.info("Starting Deployments Server ✅", port);
serve({ fetch: app.fetch, port });

10
apps/api/src/logger.ts Normal file
View File

@@ -0,0 +1,10 @@
import pino from "pino";
export const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
});

24
apps/api/src/schema.ts Normal file
View File

@@ -0,0 +1,24 @@
import { z } from "zod";
export const deployJobSchema = z.discriminatedUnion("applicationType", [
z.object({
applicationId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application"),
serverId: z.string().min(1),
}),
z.object({
composeId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("compose"),
serverId: z.string().min(1),
}),
]);
export type DeployJob = z.infer<typeof deployJobSchema>;

View File

@@ -1,28 +1,96 @@
import { LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_STORE_ID } from "."; import {
deployApplication,
deployCompose,
deployRemoteApplication,
deployRemoteCompose,
rebuildApplication,
rebuildCompose,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus,
updateCompose,
} from "@dokploy/server/dist";
import type { DeployJob } from "./schema";
import type { LemonSqueezyLicenseResponse } from "./types"; import type { LemonSqueezyLicenseResponse } from "./types";
export const validateLemonSqueezyLicense = async ( // const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
licenseKey: string, // const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
): Promise<LemonSqueezyLicenseResponse> => { // export const validateLemonSqueezyLicense = async (
try { // licenseKey: string,
const response = await fetch( // ): Promise<LemonSqueezyLicenseResponse> => {
"https://api.lemonsqueezy.com/v1/licenses/validate", // try {
{ // const response = await fetch(
method: "POST", // "https://api.lemonsqueezy.com/v1/licenses/validate",
headers: { // {
"Content-Type": "application/json", // method: "POST",
"x-api-key": LEMON_SQUEEZY_API_KEY as string, // headers: {
}, // "Content-Type": "application/json",
body: JSON.stringify({ // "x-api-key": LEMON_SQUEEZY_API_KEY as string,
license_key: licenseKey, // },
store_id: LEMON_SQUEEZY_STORE_ID as string, // body: JSON.stringify({
}), // license_key: licenseKey,
}, // store_id: LEMON_SQUEEZY_STORE_ID as string,
); // }),
// },
// );
return response.json(); // return response.json();
// } catch (error) {
// console.error("Error validating license:", error);
// return { valid: false, error: "Error validating license" };
// }
// };
export const deploy = async (job: DeployJob) => {
try {
if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "running");
if (job.server) {
if (job.type === "redeploy") {
await rebuildRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} else if (job.type === "deploy") {
await deployRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
}
}
} else if (job.applicationType === "compose") {
await updateCompose(job.composeId, {
composeStatus: "running",
});
if (job.server) {
if (job.type === "redeploy") {
await rebuildRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} else if (job.type === "deploy") {
await deployRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
}
}
}
} catch (error) { } catch (error) {
console.error("Error validating license:", error); console.log(error);
return { valid: false, error: "Error validating license" }; if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "error");
} else if (job.applicationType === "compose") {
await updateCompose(job.composeId, {
composeStatus: "error",
});
}
} }
return true;
}; };

View File

@@ -2,11 +2,12 @@
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Node",
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"types": ["node"], "outDir": "dist",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "hono/jsx" "jsxImportSource": "hono/jsx"
} },
"exclude": ["node_modules", "dist"]
} }

View File

@@ -15,13 +15,13 @@ description: '学习如何在服务器上手动安装 Dokploy。'
```bash ```bash
#!/bin/bash #!/bin/bash
# 确保以根用户身份运行脚本 # Ensure the script is run as root
if [ "$(id -u)" != "0" ]; then if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2 echo "This script must be run as root" >&2
exit 1 exit 1
fi fi
# 检查 Linux 操作系统(非 macOS 或 Docker 容器内的操作系统) # Check for Linux OS (not macOS or inside a Docker container)
if [ "$(uname)" = "Darwin" ]; then if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2 echo "This script must be run on Linux" >&2
exit 1 exit 1
@@ -32,7 +32,7 @@ if [ -f /.dockerenv ]; then
exit 1 exit 1
fi fi
# 检查端口是否被占用 # Check for occupied ports
if ss -tulnp | grep ':80 ' >/dev/null; then if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: Port 80 is already in use" >&2 echo "Error: Port 80 is already in use" >&2
exit 1 exit 1
@@ -43,32 +43,53 @@ if ss -tulnp | grep ':443 ' >/dev/null; then
exit 1 exit 1
fi fi
# 检查命令是否存在 # Function to check if a command exists
command_exists() { command_exists() {
command -v "$@" > /dev/null 2>&1 command -v "$@" > /dev/null 2>&1
} }
# 如果未安装 Docker 则安装 # Install Docker if it is not installed
if command_exists docker; then if command_exists docker; then
echo "Docker already installed" echo "Docker already installed"
else else
curl -sSL https://get.docker.com | sh curl -sSL https://get.docker.com | sh
fi fi
# 初始化 Docker Swarm # Initialize Docker Swarm
docker swarm leave --force 2>/dev/null docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
# 创建网络 get_ip() {
docker network rm -f dokploy-network 2>/dev/null # Try to get IPv4
docker network create --driver overlay --attachable dokploy-network local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
echo "Network created"
# 准备配置目录 if [ -n "$ipv4" ]; then
mkdir -p /etc/dokploy echo "$ipv4"
chmod -R 777 /etc/dokploy else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi
}
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
docker swarm init --advertise-addr $advertise_addr
if [ $? -ne 0 ]; then
echo "Error: Failed to initialize Docker Swarm" >&2
exit 1
fi
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
# Pull and deploy Dokploy # Pull and deploy Dokploy
docker pull dokploy/dokploy:latest docker pull dokploy/dokploy:latest
@@ -84,9 +105,10 @@ docker service create \
-e PORT=<Value For PORT eg(3000)> \ -e PORT=<Value For PORT eg(3000)> \
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \ -e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \ -e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
-e ADVERTISE_ADDR=$advertise_addr \
dokploy/dokploy:latest dokploy/dokploy:latest
# 输出成功消息 # Output success message
GREEN="\033[0;32m" GREEN="\033[0;32m"
YELLOW="\033[1;33m" YELLOW="\033[1;33m"
BLUE="\033[0;34m" BLUE="\033[0;34m"

View File

@@ -57,18 +57,39 @@ fi
# Initialize Docker Swarm # Initialize Docker Swarm
docker swarm leave --force 2>/dev/null docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
# Create network get_ip() {
docker network rm -f dokploy-network 2>/dev/null # Try to get IPv4
docker network create --driver overlay --attachable dokploy-network local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
echo "Network created"
# Prepare configuration directory if [ -n "$ipv4" ]; then
mkdir -p /etc/dokploy echo "$ipv4"
chmod -R 777 /etc/dokploy else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi
}
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
docker swarm init --advertise-addr $advertise_addr
if [ $? -ne 0 ]; then
echo "Error: Failed to initialize Docker Swarm" >&2
exit 1
fi
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
# Pull and deploy Dokploy # Pull and deploy Dokploy
docker pull dokploy/dokploy:latest docker pull dokploy/dokploy:latest
@@ -84,6 +105,7 @@ docker service create \
-e PORT=<Value For PORT eg(3000)> \ -e PORT=<Value For PORT eg(3000)> \
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \ -e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \ -e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
-e ADVERTISE_ADDR=$advertise_addr \
dokploy/dokploy:latest dokploy/dokploy:latest
# Output success message # Output success message

View File

@@ -31,8 +31,7 @@ The following templates are available:
- **Wordpress**: Open Source Content Management System - **Wordpress**: Open Source Content Management System
- **Open WebUI**: Free and Open Source ChatGPT Alternative - **Open WebUI**: Free and Open Source ChatGPT Alternative
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres - **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
- **Roundcube**: Free and open source webmail software for the masses, written in PHP, uses SMTP[^1].
## Create your own template ## Create your own template
@@ -41,3 +40,5 @@ We accept contributions to upload new templates to the dokploy repository.
Make sure to follow the guidelines for creating a template: Make sure to follow the guidelines for creating a template:
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates) [Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
[^1]: Please note that if you're self-hosting a mail server you need port 25 to be open for SMTP (Mail Transmission Protocol that allows you to send and receive) to work properly. Some VPS providers like [Hetzner](https://docs.hetzner.com/cloud/servers/faq/#why-can-i-not-send-any-mails-from-my-server) block this port by default for new clients.

View File

@@ -10,8 +10,10 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
p: ({ children }) => ( p: ({ children }) => (
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p> <p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
), ),
li: ({ children }) => ( li: ({ children, id }) => (
<li className="text-[#3E4342] dark:text-muted-foreground">{children}</li> <li {...{ id }} className="text-[#3E4342] dark:text-muted-foreground">
{children}
</li>
), ),
}; };
} }

View File

@@ -21,15 +21,11 @@
"react-ga4": "^2.1.0" "react-ga4": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"tsx": "4.15.7", "autoprefixer": "10.4.12",
"@biomejs/biome": "1.8.1",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"@types/node": "^20.14.2",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.1",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }
} }

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllProperties } from "@/server/utils/docker/compose"; import { addSuffixToAllProperties } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsRoot } from "@/server/utils/docker/compose/configs"; import { addSuffixToConfigsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsInServices } from "@/server/utils/docker/compose/configs"; import { addSuffixToConfigsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
addSuffixToAllConfigs, import type { ComposeSpecification } from "@dokploy/server";
addSuffixToConfigsRoot,
} from "@/server/utils/docker/compose/configs";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import type { Domain } from "@/server/api/services/domain"; import type { Domain } from "@dokploy/server";
import { createDomainLabels } from "@/server/utils/docker/domain"; import { createDomainLabels } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("createDomainLabels", () => { describe("createDomainLabels", () => {

View File

@@ -1,4 +1,4 @@
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain"; import { addDokployNetworkToRoot } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("addDokployNetworkToRoot", () => { describe("addDokployNetworkToRoot", () => {

View File

@@ -1,4 +1,4 @@
import { addDokployNetworkToService } from "@/server/utils/docker/domain"; import { addDokployNetworkToService } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("addDokployNetworkToService", () => { describe("addDokployNetworkToService", () => {

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network"; import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNetworks } from "@/server/utils/docker/compose/network"; import { addSuffixToServiceNetworks } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,10 +1,10 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import {
addSuffixToAllNetworks, addSuffixToAllNetworks,
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
} from "@/server/utils/docker/compose/network"; } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network"; import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@/server/utils/docker/compose/secrets"; import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsInServices } from "@/server/utils/docker/compose/secrets"; import { addSuffixToSecretsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllSecrets } from "@/server/utils/docker/compose/secrets"; import { addSuffixToAllSecrets } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,8 @@
import { import {
addSuffixToAllServiceNames, addSuffixToAllServiceNames,
addSuffixToServiceNames, addSuffixToServiceNames,
} from "@/server/utils/docker/compose/service"; } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service"; import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
addSuffixToAllVolumes, import type { ComposeSpecification } from "@dokploy/server";
addSuffixToVolumesRoot,
} from "@/server/utils/docker/compose/volume";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesRoot } from "@/server/utils/docker/compose/volume"; import { addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,6 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesInServices } from "@/server/utils/docker/compose/volume"; import { addSuffixToVolumesInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,9 @@
import { generateRandomHash } from "@/server/utils/docker/compose"; import { generateRandomHash } from "@dokploy/server";
import { import {
addSuffixToAllVolumes, addSuffixToAllVolumes,
addSuffixToVolumesInServices, addSuffixToVolumesInServices,
} from "@/server/utils/docker/compose/volume"; } from "@dokploy/server";
import type { ComposeSpecification } from "@/server/utils/docker/types"; import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,12 +1,23 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { paths } from "@/server/constants"; import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths(); const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@/server/utils/builders"; import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@/server/utils/builders/drop"; import { unzipDrop } from "@dokploy/server";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
// @ts-ignore
...actual,
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
};
});
if (typeof window === "undefined") { if (typeof window === "undefined") {
const undici = require("undici"); const undici = require("undici");
globalThis.File = undici.File as any; globalThis.File = undici.File as any;
@@ -81,13 +92,6 @@ const baseApp: ApplicationNested = {
username: null, username: null,
dockerContextPath: null, dockerContextPath: null,
}; };
//
vi.mock("@/server/constants", () => ({
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
// APPLICATIONS_PATH: "./__test__/drop/zips/output",
}));
describe("unzipDrop using real zip files", () => { describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths(); // const { APPLICATIONS_PATH } = paths();
@@ -102,15 +106,19 @@ describe("unzipDrop using real zip files", () => {
it("should correctly extract a zip with a single root folder", async () => { it("should correctly extract a zip with a single root folder", async () => {
baseApp.appName = "single-file"; baseApp.appName = "single-file";
// const appName = "single-file"; // const appName = "single-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); try {
const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
const zipBuffer = zip.toBuffer(); console.log(`Output Path: ${outputPath}`);
const file = new File([zipBuffer], "single.zip"); const zipBuffer = zip.toBuffer();
await unzipDrop(file, baseApp); const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true }); const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "test.txt")).toBe(true); expect(files.some((f) => f.name === "test.txt")).toBe(true);
} catch (err) {
console.log(err);
} finally {
}
}); });
it("should correctly extract a zip with a single root folder and a subfolder", async () => { it("should correctly extract a zip with a single root folder and a subfolder", async () => {

View File

@@ -1,4 +1,4 @@
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils"; import { parseRawConfig, processLogs } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`; const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;

View File

@@ -5,11 +5,12 @@ vi.mock("node:fs", () => ({
default: fs, default: fs,
})); }));
import type { Admin } from "@/server/api/services/admin"; import type { Admin, FileConfig } from "@dokploy/server";
import { createDefaultServerTraefikConfig } from "@/server/setup/traefik-setup"; import {
import { loadOrCreateConfig } from "@/server/utils/traefik/application"; createDefaultServerTraefikConfig,
import type { FileConfig } from "@/server/utils/traefik/file-types"; loadOrCreateConfig,
import { updateServerTraefik } from "@/server/utils/traefik/web-server"; updateServerTraefik,
} from "@dokploy/server";
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: Admin = { const baseAdmin: Admin = {
@@ -23,6 +24,9 @@ const baseAdmin: Admin = {
sshPrivateKey: null, sshPrivateKey: null,
enableDockerCleanup: false, enableDockerCleanup: false,
enableLogRotation: false, enableLogRotation: false,
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
}; };
beforeEach(() => { beforeEach(() => {

View File

@@ -1,7 +1,7 @@
import type { Domain } from "@/server/api/services/domain"; import type { Domain } from "@dokploy/server";
import type { Redirect } from "@/server/api/services/redirect"; import type { Redirect } from "@dokploy/server";
import type { ApplicationNested } from "@/server/utils/builders"; import type { ApplicationNested } from "@dokploy/server";
import { createRouterConfig } from "@/server/utils/traefik/domain"; import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {

View File

@@ -1,16 +1,24 @@
import path from "node:path";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
export default defineConfig({ export default defineConfig({
plugins: [
tsconfigPaths({
root: "./",
projects: ["tsconfig.json"],
}),
],
test: { test: {
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__ include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"], exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
pool: "forks", pool: "forks",
}, },
define: {
"process.env": {
NODE: "test",
},
},
resolve: {
alias: {
"@dokploy/server": path.resolve(
__dirname,
"../../../packages/server/src",
),
},
},
}); });

View File

@@ -17,7 +17,7 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input, NumberInput } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -125,28 +125,14 @@ export const UpdatePort = ({ portId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Published Port</FormLabel> <FormLabel>Published Port</FormLabel>
<FormControl> <FormControl>
<Input <NumberInput placeholder="1-65535" {...field} />
placeholder="1-65535"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(0);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="targetPort" name="targetPort"
@@ -154,22 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Target Port</FormLabel> <FormLabel>Target Port</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="1-65535" {...field} />
placeholder="1-65535"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(0);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -80,7 +80,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -16,20 +16,37 @@ interface Props {
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
const [data, setData] = useState(""); const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null); const endOfLogsRef = useRef<HTMLDivElement>(null);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
useEffect(() => { useEffect(() => {
if (!open || !logPath) return; if (!open || !logPath) return;
setData("");
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`; const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws; // Store WebSocket instance in ref
ws.onmessage = (e) => { ws.onmessage = (e) => {
setData((currentData) => currentData + e.data); setData((currentData) => currentData + e.data);
}; };
return () => ws.close(); ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onclose = () => {
console.log("WebSocket connection closed");
wsRef.current = null; // Clear reference on close
};
return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
};
}, [logPath, open]); }, [logPath, open]);
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -45,7 +62,15 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
open={open} open={open}
onOpenChange={(e) => { onOpenChange={(e) => {
onClose(); onClose();
if (!e) setData(""); if (!e) {
setData("");
}
if (wsRef.current) {
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
}
}} }}
> >
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}> <DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>

View File

@@ -18,7 +18,7 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input, NumberInput } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -228,13 +228,7 @@ export const AddDomain = ({
<FormItem> <FormItem>
<FormLabel>Container Port</FormLabel> <FormLabel>Container Port</FormLabel>
<FormControl> <FormControl>
<Input <NumberInput placeholder={"3000"} {...field} />
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -52,7 +52,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
<div className="flex w-full flex-col items-center justify-center gap-3"> <div className="flex w-full flex-col items-center justify-center gap-3">
<GlobeIcon className="size-8 text-muted-foreground" /> <GlobeIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To access to the application is required to set at least 1 To access the application it is required to set at least 1
domain domain
</span> </span>
<div className="flex flex-row gap-4 flex-wrap"> <div className="flex flex-row gap-4 flex-wrap">

View File

@@ -21,20 +21,38 @@ export const ShowDeploymentCompose = ({
}: Props) => { }: Props) => {
const [data, setData] = useState(""); const [data, setData] = useState("");
const endOfLogsRef = useRef<HTMLDivElement>(null); const endOfLogsRef = useRef<HTMLDivElement>(null);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
useEffect(() => { useEffect(() => {
if (!open || !logPath) return; if (!open || !logPath) return;
setData("");
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`; const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws; // Store WebSocket instance in ref
ws.onmessage = (e) => { ws.onmessage = (e) => {
setData((currentData) => currentData + e.data); setData((currentData) => currentData + e.data);
}; };
return () => ws.close(); ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onclose = () => {
console.log("WebSocket connection closed");
wsRef.current = null;
};
return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
};
}, [logPath, open]); }, [logPath, open]);
const scrollToBottom = () => { const scrollToBottom = () => {
@@ -50,7 +68,15 @@ export const ShowDeploymentCompose = ({
open={open} open={open}
onOpenChange={(e) => { onOpenChange={(e) => {
onClose(); onClose();
if (!e) setData(""); if (!e) {
setData("");
}
if (wsRef.current) {
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
}
}} }}
> >
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}> <DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>

View File

@@ -18,7 +18,7 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input, NumberInput } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -364,13 +364,7 @@ export const AddDomainCompose = ({
<FormItem> <FormItem>
<FormLabel>Container Port</FormLabel> <FormLabel>Container Port</FormLabel>
<FormControl> <FormControl>
<Input <NumberInput placeholder={"3000"} {...field} />
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -53,7 +53,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
<div className="flex w-full flex-col items-center justify-center gap-3"> <div className="flex w-full flex-col items-center justify-center gap-3">
<GlobeIcon className="size-8 text-muted-foreground" /> <GlobeIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To access to the application is required to set at least 1 To access to the application it is required to set at least 1
domain domain
</span> </span>
<div className="flex flex-row gap-4 flex-wrap"> <div className="flex flex-row gap-4 flex-wrap">

View File

@@ -77,7 +77,6 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
}); });
}) })
.catch((e) => { .catch((e) => {
console.log(e);
toast.error("Error to update the compose config"); toast.error("Error to update the compose config");
}); });
}; };

View File

@@ -11,7 +11,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Puzzle, RefreshCw } from "lucide-react"; import { Puzzle, RefreshCw } from "lucide-react";
import { useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
interface Props { interface Props {
@@ -34,6 +34,16 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation(); const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
useEffect(() => {
if (isOpen) {
mutateAsync({ composeId })
.then(() => {
refetch();
})
.catch((err) => {});
}
}, [isOpen]);
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>

View File

@@ -1,7 +1,7 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import React, { useEffect } from "react"; import React, { useEffect, useRef } from "react";
import { FitAddon } from "xterm-addon-fit"; import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css"; import "@xterm/xterm/css/xterm.css";
@@ -18,12 +18,24 @@ export const DockerLogsId: React.FC<Props> = ({
}) => { }) => {
const [term, setTerm] = React.useState<Terminal>(); const [term, setTerm] = React.useState<Terminal>();
const [lines, setLines] = React.useState<number>(40); const [lines, setLines] = React.useState<number>(40);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
const createTerminal = (): Terminal => { useEffect(() => {
// if (containerId === "select-a-container") {
// return;
// }
const container = document.getElementById(id); const container = document.getElementById(id);
if (container) { if (container) {
container.innerHTML = ""; container.innerHTML = "";
} }
if (wsRef.current) {
console.log(wsRef.current);
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close();
}
wsRef.current = null;
}
const termi = new Terminal({ const termi = new Terminal({
cursorBlink: true, cursorBlink: true,
cols: 80, cols: 80,
@@ -45,7 +57,7 @@ export const DockerLogsId: React.FC<Props> = ({
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`; const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws;
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
termi.loadAddon(fitAddon); termi.loadAddon(fitAddon);
// @ts-ignore // @ts-ignore
@@ -54,6 +66,10 @@ export const DockerLogsId: React.FC<Props> = ({
termi.focus(); termi.focus();
setTerm(termi); setTerm(termi);
ws.onerror = (error) => {
console.error("WebSocket error: ", error);
};
ws.onmessage = (e) => { ws.onmessage = (e) => {
termi.write(e.data); termi.write(e.data);
}; };
@@ -62,12 +78,14 @@ export const DockerLogsId: React.FC<Props> = ({
console.log(e.reason); console.log(e.reason);
termi.write(`Connection closed!\nReason: ${e.reason}\n`); termi.write(`Connection closed!\nReason: ${e.reason}\n`);
wsRef.current = null;
};
return () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
ws.close();
wsRef.current = null;
}
}; };
return termi;
};
useEffect(() => {
createTerminal();
}, [lines, containerId]); }, [lines, containerId]);
useEffect(() => { useEffect(() => {

View File

@@ -79,7 +79,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -44,7 +44,7 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle> <CardTitle className="text-xl">Backups</CardTitle>
<CardDescription> <CardDescription>
Add backup to your database to save the data to a different Add backups to your database to save the data to a different
providers. providers.
</CardDescription> </CardDescription>
</div> </div>
@@ -62,8 +62,8 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please, To create a backup it is required to set at least 1 provider.
go to{" "} Please, go to{" "}
<Link <Link
href="/dashboard/settings/server" href="/dashboard/settings/server"
className="text-foreground" className="text-foreground"

View File

@@ -48,6 +48,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId }); const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
@@ -79,7 +80,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`; return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
@@ -90,7 +91,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
form, form,
data?.databaseName, data?.databaseName,
data?.databaseUser, data?.databaseUser,
ip, getIp,
]); ]);
return ( return (
<> <>

View File

@@ -79,7 +79,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -44,8 +44,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle> <CardTitle className="text-xl">Backups</CardTitle>
<CardDescription> <CardDescription>
Add backup to your database to save the data to a different Add backups to your database to save the data to a different
providers. provider.
</CardDescription> </CardDescription>
</div> </div>
@@ -62,8 +62,8 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please, To create a backup it is required to set at least 1 provider.
go to{" "} Please, go to{" "}
<Link <Link
href="/dashboard/settings/server" href="/dashboard/settings/server"
className="text-foreground" className="text-foreground"

View File

@@ -48,7 +48,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const { data, refetch } = api.mongo.one.useQuery({ mongoId }); const { data, refetch } = api.mongo.one.useQuery({ mongoId });
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
@@ -80,7 +80,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}`; return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
@@ -90,7 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseUser, data?.databaseUser,
ip, getIp,
]); ]);
return ( return (

View File

@@ -30,7 +30,7 @@ export const ShowVolumes = ({ mongoId }: Props) => {
<div> <div>
<CardTitle className="text-xl">Volumes</CardTitle> <CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription> <CardDescription>
If you want to persist data in this mongo use the following config If you want to persist data in this mongo use the following config.
to setup the volumes to setup the volumes
</CardDescription> </CardDescription>
</div> </div>

View File

@@ -191,7 +191,7 @@ export const DockerMonitoring = ({
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Monitoring</CardTitle> <CardTitle className="text-xl">Monitoring</CardTitle>
<CardDescription> <CardDescription>
Watch the usage of your server in the current app Watch the usage of your server in the current app.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">

View File

@@ -79,7 +79,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -44,8 +44,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle> <CardTitle className="text-xl">Backups</CardTitle>
<CardDescription> <CardDescription>
Add backup to your database to save the data to a different Add backups to your database to save the data to a different
providers. provider.
</CardDescription> </CardDescription>
</div> </div>
@@ -62,8 +62,8 @@ export const ShowBackupMySql = ({ mysqlId }: Props) => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please, To create a backup it is required to set at least 1 provider.
go to{" "} Please, go to{" "}
<Link <Link
href="/dashboard/settings/server" href="/dashboard/settings/server"
className="text-foreground" className="text-foreground"

View File

@@ -48,7 +48,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data, refetch } = api.mysql.one.useQuery({ mysqlId }); const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
resolver: zodResolver(DockerProviderSchema), resolver: zodResolver(DockerProviderSchema),
@@ -80,7 +80,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`; return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
@@ -91,7 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
data?.databaseName, data?.databaseName,
data?.databaseUser, data?.databaseUser,
form, form,
ip, getIp,
]); ]);
return ( return (
<> <>

View File

@@ -79,7 +79,7 @@ export const ShowPostgresResources = ({ postgresId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -45,8 +45,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle> <CardTitle className="text-xl">Backups</CardTitle>
<CardDescription> <CardDescription>
Add backup to your database to save the data to a different Add backups to your database to save the data to a different
providers. provider.
</CardDescription> </CardDescription>
</div> </div>
@@ -63,8 +63,8 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please, To create a backup it is required to set at least 1 provider.
go to{" "} Please, go to{" "}
<Link <Link
href="/dashboard/settings/server" href="/dashboard/settings/server"
className="text-foreground" className="text-foreground"

View File

@@ -48,6 +48,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
const { data, refetch } = api.postgres.one.useQuery({ postgresId }); const { data, refetch } = api.postgres.one.useQuery({ postgresId });
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.postgres.saveExternalPort.useMutation(); api.postgres.saveExternalPort.useMutation();
const getIp = data?.server?.ipAddress || ip;
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
@@ -79,10 +80,9 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
useEffect(() => { useEffect(() => {
const buildConnectionUrl = () => { const buildConnectionUrl = () => {
const hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`; return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
@@ -92,7 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseName, data?.databaseName,
ip, getIp,
]); ]);
return ( return (

View File

@@ -39,7 +39,7 @@ import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react"; import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -71,6 +71,7 @@ interface Props {
export const AddCompose = ({ projectId, projectName }: Props) => { export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName); const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
@@ -101,6 +102,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Compose Created"); toast.success("Compose Created");
setVisible(false);
await utils.project.one.invalidate({ await utils.project.one.invalidate({
projectId, projectId,
}); });
@@ -111,7 +113,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
}; };
return ( return (
<Dialog> <Dialog open={visible} onOpenChange={setVisible}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer space-x-3" className="w-full cursor-pointer space-x-3"

View File

@@ -15,20 +15,25 @@ import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
AlertTriangle, AlertTriangle,
BookIcon, BookIcon,
CircuitBoard,
ExternalLink,
ExternalLinkIcon, ExternalLinkIcon,
FolderInput, FolderInput,
MoreHorizontalIcon, MoreHorizontalIcon,
TrashIcon, TrashIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Fragment } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { UpdateProject } from "./update"; import { UpdateProject } from "./update";
@@ -45,6 +50,7 @@ export const ShowProjects = () => {
}, },
); );
const { mutateAsync } = api.project.remove.useMutation(); const { mutateAsync } = api.project.remove.useMutation();
return ( return (
<> <>
{data?.length === 0 && ( {data?.length === 0 && (
@@ -74,17 +80,87 @@ export const ShowProjects = () => {
project?.redis.length + project?.redis.length +
project?.applications.length + project?.applications.length +
project?.compose.length; project?.compose.length;
const flattedDomains = [
...project.applications.flatMap((a) => a.domains),
...project.compose.flatMap((a) => a.domains),
];
const renderDomainsDropdown = (
item: typeof project.compose | typeof project.applications,
) =>
item[0] ? (
<DropdownMenuGroup>
<DropdownMenuLabel>
{"applicationId" in item[0] ? "Applications" : "Compose"}
</DropdownMenuLabel>
{item.map((a) => (
<Fragment
key={"applicationId" in a ? a.applicationId : a.composeId}
>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs ">
{a.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{a.domains.map((domain) => (
<DropdownMenuItem key={domain.domainId} asChild>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<ExternalLink className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</Fragment>
))}
</DropdownMenuGroup>
) : null;
return ( return (
<div key={project.projectId} className="w-full lg:max-w-md"> <div key={project.projectId} className="w-full lg:max-w-md">
<Link href={`/dashboard/project/${project.projectId}`}> <Link href={`/dashboard/project/${project.projectId}`}>
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card"> <Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
<Button {flattedDomains.length > 1 ? (
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100" <DropdownMenu>
size="sm" <DropdownMenuTrigger asChild>
variant="default" <Button
> className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
<ExternalLinkIcon className="size-3.5" /> size="sm"
</Button> variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2"
onClick={(e) => e.stopPropagation()}
>
{renderDomainsDropdown(project.applications)}
{renderDomainsDropdown(project.compose)}
</DropdownMenuContent>
</DropdownMenu>
) : flattedDomains[0] ? (
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
onClick={(e) => e.stopPropagation()}
>
<Link
href={`${flattedDomains[0].https ? "https" : "http"}://${flattedDomains[0].host}${flattedDomains[0].path}`}
target="_blank"
>
<ExternalLinkIcon className="size-3.5" />
</Link>
</Button>
) : null}
<CardHeader> <CardHeader>
<CardTitle className="flex items-center justify-between gap-2"> <CardTitle className="flex items-center justify-between gap-2">
<span className="flex flex-col gap-1.5"> <span className="flex flex-col gap-1.5">

View File

@@ -79,7 +79,7 @@ export const ShowRedisResources = ({ redisId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Resources</CardTitle> <CardTitle className="text-xl">Resources</CardTitle>
<CardDescription> <CardDescription>
If you want to decrease or increase the resources to a specific If you want to decrease or increase the resources to a specific.
application or database application or database
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View File

@@ -48,6 +48,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const { data, refetch } = api.redis.one.useQuery({ redisId }); const { data, refetch } = api.redis.one.useQuery({ redisId });
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation(); const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState(""); const [connectionUrl, setConnectionUrl] = useState("");
const getIp = data?.server?.ipAddress || ip;
const form = useForm<DockerProvider>({ const form = useForm<DockerProvider>({
defaultValues: {}, defaultValues: {},
@@ -81,11 +82,11 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
const hostname = window.location.hostname; const hostname = window.location.hostname;
const port = form.watch("externalPort") || data?.externalPort; const port = form.watch("externalPort") || data?.externalPort;
return `redis://default:${data?.databasePassword}@${ip}:${port}`; return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
}, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]); }, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">

View File

@@ -0,0 +1,241 @@
import { Button } from "@/components/ui/button";
import { NumberInput } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { loadStripe } from "@stripe/stripe-js";
import clsx from "clsx";
import { AlertTriangle, CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
import React, { useState } from "react";
const stripePromise = loadStripe(
"pk_test_51QAm7bF3cxQuHeOz0xg04o9teeyTbbNHQPJ5Tr98MlTEan9MzewT3gwh0jSWBNvrRWZ5vASoBgxUSF4gPWsJwATk00Ir2JZ0S1",
);
export const calculatePrice = (count: number, isAnnual = false) => {
if (isAnnual) {
if (count <= 1) return 45.9;
return 35.7 * count;
}
if (count <= 1) return 4.5;
return count * 3.5;
};
// 178.156.147.118
export const ShowBilling = () => {
const { data: servers } = api.server.all.useQuery(undefined);
const { data: admin } = api.admin.one.useQuery();
const { data } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation();
const { mutateAsync: createCustomerPortalSession } =
api.stripe.createCustomerPortalSession.useMutation();
const [serverQuantity, setServerQuantity] = useState(3);
const [isAnnual, setIsAnnual] = useState(false);
const handleCheckout = async (productId: string) => {
const stripe = await stripePromise;
if (data && data.subscriptions.length === 0) {
createCheckoutSession({
productId,
serverQuantity: serverQuantity,
isAnnual,
}).then(async (session) => {
await stripe?.redirectToCheckout({
sessionId: session.sessionId,
});
});
}
};
const products = data?.products.filter((product) => {
// @ts-ignore
const interval = product?.default_price?.recurring?.interval;
return isAnnual ? interval === "year" : interval === "month";
});
const maxServers = admin?.serversQuantity ?? 1;
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
const safePercentage = Math.min(percentage, 100);
return (
<div className="flex flex-col gap-4 w-full">
<Tabs
defaultValue="monthly"
value={isAnnual ? "annual" : "monthly"}
className="w-full"
onValueChange={(e) => setIsAnnual(e === "annual")}
>
<TabsList>
<TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="annual">Annual</TabsTrigger>
</TabsList>
</Tabs>
{admin?.stripeSubscriptionId && (
<div className="space-y-2">
<h3 className="text-lg font-medium">Servers Plan</h3>
<p className="text-sm text-muted-foreground">
You have {servers?.length} server on your plan of{" "}
{admin?.serversQuantity} servers
</p>
<div className="pb-5">
<Progress value={safePercentage} className="max-w-lg" />
</div>
{admin && (
<>
{admin.serversQuantity! <= servers?.length! && (
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">
You have reached the maximum number of servers you can
create, please upgrade your plan to add more servers.
</span>
</div>
)}
</>
)}
</div>
)}
{products?.map((product) => {
const featured = true;
return (
<div key={product.id}>
<section
className={clsx(
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
featured
? "order-first bg-black border py-8 lg:order-none"
: "lg:py-8",
)}
>
{isAnnual ? (
<div className="flex flex-row gap-2 items-center">
<p className=" text-2xl font-semibold tracking-tight text-primary ">
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
</p>
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
${" "}
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(2)}{" "}
/ Month USD
</p>
</div>
) : (
<p className=" text-2xl font-semibold tracking-tight text-primary ">
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
</p>
)}
<h3 className="mt-5 font-medium text-lg text-white">
{product.name}
</h3>
<p
className={clsx(
"text-sm",
featured ? "text-white" : "text-slate-400",
)}
>
{product.description}
</p>
<ul
role="list"
className={clsx(
" mt-4 flex flex-col gap-y-2 text-sm",
featured ? "text-white" : "text-slate-200",
)}
>
{[
"All the features of Dokploy",
"Unlimited deployments",
"Self-hosted on your own infrastructure",
"Full access to all deployment features",
"Dokploy integration",
"Backups",
"All Incoming features",
].map((feature) => (
<li key={feature} className="flex text-muted-foreground">
<CheckIcon />
<span className="ml-4">{feature}</span>
</li>
))}
</ul>
<div className="flex flex-col gap-2 mt-4">
<div className="flex items-center gap-2 justify-center">
<span className="text-sm text-muted-foreground">
{serverQuantity} Servers
</span>
</div>
<div className="flex items-center space-x-2">
<Button
disabled={serverQuantity <= 1}
variant="outline"
onClick={() => {
if (serverQuantity <= 1) return;
setServerQuantity(serverQuantity - 1);
}}
>
<MinusIcon className="h-4 w-4" />
</Button>
<NumberInput
value={serverQuantity}
onChange={(e) => {
setServerQuantity(e.target.value as unknown as number);
}}
/>
<Button
variant="outline"
onClick={() => {
setServerQuantity(serverQuantity + 1);
}}
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
<div
className={cn(
data?.subscriptions && data?.subscriptions?.length > 0
? "justify-between"
: "justify-end",
"flex flex-row items-center gap-2 mt-4",
)}
>
{admin?.stripeCustomerId && (
<Button
variant="secondary"
className="w-full"
onClick={async () => {
const session = await createCustomerPortalSession();
window.open(session.url);
}}
>
Manage Subscription
</Button>
)}
{data?.subscriptions?.length === 0 && (
<div className="justify-end w-full">
<Button
className="w-full"
onClick={async () => {
handleCheckout(product.id);
}}
disabled={serverQuantity < 1}
>
Subscribe
</Button>
</div>
)}
</div>
</div>
</section>
</div>
);
})}
</div>
);
};

View File

@@ -18,10 +18,25 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle, HelpCircle } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -35,6 +50,7 @@ const addCertificate = z.object({
certificateData: z.string().min(1, "Certificate data is required"), certificateData: z.string().min(1, "Certificate data is required"),
privateKey: z.string().min(1, "Private key is required"), privateKey: z.string().min(1, "Private key is required"),
autoRenew: z.boolean().optional(), autoRenew: z.boolean().optional(),
serverId: z.string().optional(),
}); });
type AddCertificate = z.infer<typeof addCertificate>; type AddCertificate = z.infer<typeof addCertificate>;
@@ -44,6 +60,7 @@ export const AddCertificate = () => {
const { mutateAsync, isError, error, isLoading } = const { mutateAsync, isError, error, isLoading } =
api.certificates.create.useMutation(); api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const form = useForm<AddCertificate>({ const form = useForm<AddCertificate>({
defaultValues: { defaultValues: {
@@ -64,6 +81,7 @@ export const AddCertificate = () => {
certificateData: data.certificateData, certificateData: data.certificateData,
privateKey: data.privateKey, privateKey: data.privateKey,
autoRenew: data.autoRenew, autoRenew: data.autoRenew,
serverId: data.serverId,
}) })
.then(async () => { .then(async () => {
toast.success("Certificate Created"); toast.success("Certificate Created");
@@ -144,6 +162,47 @@ export const AddCertificate = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form> </form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3"> <DialogFooter className="flex w-full flex-row !justify-between pt-3">

View File

@@ -27,7 +27,8 @@ export const ShowCertificates = () => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<ShieldCheck className="size-8 self-center text-muted-foreground" /> <ShieldCheck className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a certificate is required to upload your certificate To create a certificate it is required to upload an existing
certificate
</span> </span>
<AddCertificate /> <AddCertificate />
</div> </div>

View File

@@ -17,10 +17,18 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Container } from "lucide-react"; import { AlertTriangle, Container } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -36,10 +44,9 @@ const AddRegistrySchema = z.object({
password: z.string().min(1, { password: z.string().min(1, {
message: "Password is required", message: "Password is required",
}), }),
registryUrl: z.string().min(1, { registryUrl: z.string(),
message: "Registry URL is required",
}),
imagePrefix: z.string(), imagePrefix: z.string(),
serverId: z.string().optional(),
}); });
type AddRegistry = z.infer<typeof AddRegistrySchema>; type AddRegistry = z.infer<typeof AddRegistrySchema>;
@@ -48,9 +55,9 @@ export const AddRegistry = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.registry.create.useMutation(); const { mutateAsync, error, isError } = api.registry.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync: testRegistry, isLoading } = const { mutateAsync: testRegistry, isLoading } =
api.registry.testRegistry.useMutation(); api.registry.testRegistry.useMutation();
const router = useRouter();
const form = useForm<AddRegistry>({ const form = useForm<AddRegistry>({
defaultValues: { defaultValues: {
username: "", username: "",
@@ -58,6 +65,7 @@ export const AddRegistry = () => {
registryUrl: "", registryUrl: "",
imagePrefix: "", imagePrefix: "",
registryName: "", registryName: "",
serverId: "",
}, },
resolver: zodResolver(AddRegistrySchema), resolver: zodResolver(AddRegistrySchema),
}); });
@@ -67,6 +75,7 @@ export const AddRegistry = () => {
const registryUrl = form.watch("registryUrl"); const registryUrl = form.watch("registryUrl");
const registryName = form.watch("registryName"); const registryName = form.watch("registryName");
const imagePrefix = form.watch("imagePrefix"); const imagePrefix = form.watch("imagePrefix");
const serverId = form.watch("serverId");
useEffect(() => { useEffect(() => {
form.reset({ form.reset({
@@ -74,6 +83,7 @@ export const AddRegistry = () => {
password: "", password: "",
registryUrl: "", registryUrl: "",
imagePrefix: "", imagePrefix: "",
serverId: "",
}); });
}, [form, form.reset, form.formState.isSubmitSuccessful]); }, [form, form.reset, form.formState.isSubmitSuccessful]);
@@ -85,6 +95,7 @@ export const AddRegistry = () => {
registryUrl: data.registryUrl, registryUrl: data.registryUrl,
registryType: "cloud", registryType: "cloud",
imagePrefix: data.imagePrefix, imagePrefix: data.imagePrefix,
serverId: data.serverId,
}) })
.then(async (data) => { .then(async (data) => {
await utils.registry.all.invalidate(); await utils.registry.all.invalidate();
@@ -211,34 +222,77 @@ export const AddRegistry = () => {
)} )}
/> />
</div> </div>
<DialogFooter className="flex flex-row w-full sm:justify-between gap-4 flex-wrap"> <DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
<Button <div className="flex flex-col gap-4 border p-2 rounded-lg">
type="button" <span className="text-sm text-muted-foreground">
variant={"secondary"} Select a server to test the registry. If you don't have a
isLoading={isLoading} server choose the default one.
onClick={async () => { </span>
await testRegistry({ <FormField
username: username, control={form.control}
password: password, name="serverId"
registryUrl: registryUrl, render={({ field }) => (
registryName: registryName, <FormItem>
registryType: "cloud", <FormLabel>Server (Optional)</FormLabel>
imagePrefix: imagePrefix, <FormControl>
}) <Select
.then((data) => { onValueChange={field.onChange}
if (data) { defaultValue={field.value}
toast.success("Registry Tested Successfully"); >
} else { <SelectTrigger className="w-full">
toast.error("Registry Test Failed"); <SelectValue placeholder="Select a server" />
} </SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testRegistry({
username: username,
password: password,
registryUrl: registryUrl,
registryName: registryName,
registryType: "cloud",
imagePrefix: imagePrefix,
serverId: serverId,
}) })
.catch(() => { .then((data) => {
toast.error("Error to test the registry"); if (data) {
}); toast.success("Registry Tested Successfully");
}} } else {
> toast.error("Registry Test Failed");
Test Registry }
</Button> })
.catch(() => {
toast.error("Error to test the registry");
});
}}
>
Test Registry
</Button>
</div>
<Button isLoading={form.formState.isSubmitting} type="submit"> <Button isLoading={form.formState.isSubmitting} type="submit">
Create Create
</Button> </Button>

View File

@@ -1,181 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Container } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddRegistrySchema = z.object({
username: z
.string()
.min(1, {
message: "Username is required",
})
.regex(/^[a-zA-Z0-9]+$/, {
message: "Username can only contain letters and numbers",
}),
password: z.string().min(1, {
message: "Password is required",
}),
registryUrl: z.string().min(1, {
message: "Registry URL is required",
}),
});
type AddRegistry = z.infer<typeof AddRegistrySchema>;
export const AddSelfHostedRegistry = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError, isLoading } =
api.registry.enableSelfHostedRegistry.useMutation();
const router = useRouter();
const form = useForm<AddRegistry>({
defaultValues: {
username: "",
password: "",
registryUrl: "",
},
resolver: zodResolver(AddRegistrySchema),
});
useEffect(() => {
form.reset({
registryUrl: "",
username: "",
password: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddRegistry) => {
await mutateAsync({
registryUrl: data.registryUrl,
username: data.username,
password: data.password,
})
.then(async (data) => {
await utils.registry.all.invalidate();
toast.success("Self Hosted Registry Created");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create a self hosted registry");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="max-sm:w-full">
<Container className="h-4 w-4" />
Enable Self Hosted Registry
</Button>
</DialogTrigger>
<DialogContent className="sm:m:max-w-lg ">
<DialogHeader>
<DialogTitle>Add a self hosted registry</DialogTitle>
<DialogDescription>
Fill the next fields to add a self hosted registry.
</DialogDescription>
</DialogHeader>
{isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="Password"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="registryUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Registry URL</FormLabel>
<FormControl>
<Input placeholder="registry.dokploy.com" {...field} />
</FormControl>
<FormDescription>
Point a DNS record to the VPS IP address.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<Button isLoading={isLoading} type="submit">
Create
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -8,7 +8,6 @@ import {
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Server } from "lucide-react"; import { Server } from "lucide-react";
import { AddRegistry } from "./add-docker-registry"; import { AddRegistry } from "./add-docker-registry";
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
import { DeleteRegistry } from "./delete-registry"; import { DeleteRegistry } from "./delete-registry";
import { UpdateDockerRegistry } from "./update-docker-registry"; import { UpdateDockerRegistry } from "./update-docker-registry";
@@ -31,8 +30,6 @@ export const ShowRegistry = () => {
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
{data && data?.length > 0 && ( {data && data?.length > 0 && (
<> <>
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
<AddRegistry /> <AddRegistry />
</> </>
)} )}
@@ -43,11 +40,10 @@ export const ShowRegistry = () => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<Server className="size-8 self-center text-muted-foreground" /> <Server className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground text-center"> <span className="text-base text-muted-foreground text-center">
To create a cluster is required to set a registry. To create a cluster it is required to set a registry.
</span> </span>
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center"> <div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
<AddSelfHostedRegistry />
<AddRegistry /> <AddRegistry />
</div> </div>
</div> </div>

View File

@@ -17,6 +17,15 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -34,10 +43,9 @@ const updateRegistry = z.object({
message: "Username is required", message: "Username is required",
}), }),
password: z.string(), password: z.string(),
registryUrl: z.string().min(1, { registryUrl: z.string(),
message: "Registry URL is required",
}),
imagePrefix: z.string(), imagePrefix: z.string(),
serverId: z.string().optional(),
}); });
type UpdateRegistry = z.infer<typeof updateRegistry>; type UpdateRegistry = z.infer<typeof updateRegistry>;
@@ -48,6 +56,8 @@ interface Props {
export const UpdateDockerRegistry = ({ registryId }: Props) => { export const UpdateDockerRegistry = ({ registryId }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync: testRegistry, isLoading } = const { mutateAsync: testRegistry, isLoading } =
api.registry.testRegistry.useMutation(); api.registry.testRegistry.useMutation();
const { data, refetch } = api.registry.one.useQuery( const { data, refetch } = api.registry.one.useQuery(
@@ -69,15 +79,19 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: "", username: "",
password: "", password: "",
registryUrl: "", registryUrl: "",
serverId: "",
}, },
resolver: zodResolver(updateRegistry), resolver: zodResolver(updateRegistry),
}); });
console.log(form.formState.errors);
const password = form.watch("password"); const password = form.watch("password");
const username = form.watch("username"); const username = form.watch("username");
const registryUrl = form.watch("registryUrl"); const registryUrl = form.watch("registryUrl");
const registryName = form.watch("registryName"); const registryName = form.watch("registryName");
const imagePrefix = form.watch("imagePrefix"); const imagePrefix = form.watch("imagePrefix");
const serverId = form.watch("serverId");
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -87,6 +101,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: data.username || "", username: data.username || "",
password: "", password: "",
registryUrl: data.registryUrl || "", registryUrl: data.registryUrl || "",
serverId: "",
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
@@ -99,6 +114,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
username: data.username, username: data.username,
registryUrl: data.registryUrl, registryUrl: data.registryUrl,
imagePrefix: data.imagePrefix, imagePrefix: data.imagePrefix,
serverId: data.serverId,
}) })
.then(async (data) => { .then(async (data) => {
toast.success("Registry Updated"); toast.success("Registry Updated");
@@ -224,13 +240,47 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
</div> </div>
</form> </form>
<DialogFooter <DialogFooter className="flex flex-col w-full sm:justify-between gap-4 flex-wrap sm:flex-col">
className={cn( <div className="flex flex-col gap-4 border p-2 rounded-lg">
isCloud ? "sm:justify-between " : "", <span className="text-sm text-muted-foreground">
"flex flex-row w-full gap-4 flex-wrap", Select a server to test the registry. If you don't have a server
)} choose the default one.
> </span>
{isCloud && ( <FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Server (Optional)</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button <Button
type="button" type="button"
variant={"secondary"} variant={"secondary"}
@@ -243,6 +293,7 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
registryName: registryName, registryName: registryName,
registryType: "cloud", registryType: "cloud",
imagePrefix: imagePrefix, imagePrefix: imagePrefix,
serverId: serverId,
}) })
.then((data) => { .then((data) => {
if (data) { if (data) {
@@ -258,12 +309,12 @@ export const UpdateDockerRegistry = ({ registryId }: Props) => {
> >
Test Registry Test Registry
</Button> </Button>
)} </div>
<Button <Button
isLoading={form.formState.isSubmitting} isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit" type="submit"
form="hook-form"
> >
Update Update
</Button> </Button>

View File

@@ -18,6 +18,16 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -32,12 +42,15 @@ const addDestination = z.object({
bucket: z.string(), bucket: z.string(),
region: z.string(), region: z.string(),
endpoint: z.string(), endpoint: z.string(),
serverId: z.string().optional(),
}); });
type AddDestination = z.infer<typeof addDestination>; type AddDestination = z.infer<typeof addDestination>;
export const AddDestination = () => { export const AddDestination = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync, isError, error, isLoading } = const { mutateAsync, isError, error, isLoading } =
api.destination.create.useMutation(); api.destination.create.useMutation();
@@ -189,30 +202,106 @@ export const AddDestination = () => {
/> />
</form> </form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3"> <DialogFooter
<Button className={cn(
isLoading={isLoadingConnection} isCloud ? "!flex-col" : "flex-row",
type="button" "flex w-full !justify-between pt-3 gap-4",
variant="secondary" )}
onClick={async () => { >
await testConnection({ {isCloud ? (
accessKey: form.getValues("accessKeyId"), <div className="flex flex-col gap-4 border p-2 rounded-lg">
bucket: form.getValues("bucket"), <span className="text-sm text-muted-foreground">
endpoint: form.getValues("endpoint"), Select a server to test the destination. If you don't have a
name: "Test", server choose the default one.
region: form.getValues("region"), </span>
secretAccessKey: form.getValues("secretAccessKey"), <FormField
}) control={form.control}
.then(async () => { name="serverId"
toast.success("Connection Success"); render={({ field }) => (
<FormItem>
<FormLabel>Server (Optional)</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
serverId: form.getValues("serverId"),
})
.then(async () => {
toast.success("Connection Success");
})
.catch((e) => {
toast.error("Error to connect the provider", {
description: e.message,
});
});
}}
>
Test Connection
</Button>
</div>
) : (
<Button
isLoading={isLoadingConnection}
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
}) })
.catch(() => { .then(async () => {
toast.error("Error to connect the provider"); toast.success("Connection Success");
}); })
}} .catch(() => {
> toast.error("Error to connect the provider");
Test connection });
</Button> }}
>
Test connection
</Button>
)}
<Button <Button
isLoading={isLoading} isLoading={isLoading}
form="hook-form-destination-add" form="hook-form-destination-add"

View File

@@ -29,7 +29,7 @@ export const ShowDestinations = () => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<FolderUp className="size-8 self-center text-muted-foreground" /> <FolderUp className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. To create a backup it is required to set at least 1 provider.
</span> </span>
<AddDestination /> <AddDestination />
</div> </div>

View File

@@ -18,6 +18,16 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react"; import { PenBoxIcon } from "lucide-react";
@@ -33,6 +43,7 @@ const updateDestination = z.object({
bucket: z.string(), bucket: z.string(),
region: z.string(), region: z.string(),
endpoint: z.string(), endpoint: z.string(),
serverId: z.string().optional(),
}); });
type UpdateDestination = z.infer<typeof updateDestination>; type UpdateDestination = z.infer<typeof updateDestination>;
@@ -43,6 +54,8 @@ interface Props {
export const UpdateDestination = ({ destinationId }: Props) => { export const UpdateDestination = ({ destinationId }: Props) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { data, refetch } = api.destination.one.useQuery( const { data, refetch } = api.destination.one.useQuery(
{ {
@@ -220,34 +233,107 @@ export const UpdateDestination = ({ destinationId }: Props) => {
</div> </div>
</form> </form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3"> <DialogFooter
<Button className={cn(
isLoading={isLoadingConnection} isCloud ? "!flex-col" : "flex-row",
type="button" "flex w-full !justify-between pt-3 gap-4",
variant="secondary" )}
onClick={async () => { >
await testConnection({ {isCloud ? (
accessKey: form.getValues("accessKeyId"), <div className="flex flex-col gap-4 border p-2 rounded-lg">
bucket: form.getValues("bucket"), <span className="text-sm text-muted-foreground">
endpoint: form.getValues("endpoint"), Select a server to test the destination. If you don't have a
name: "Test", server choose the default one.
region: form.getValues("region"), </span>
secretAccessKey: form.getValues("secretAccessKey"), <FormField
}) control={form.control}
.then(async () => { name="serverId"
toast.success("Connection Success"); render={({ field }) => (
<FormItem>
<FormLabel>Server (Optional)</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant={"secondary"}
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
serverId: form.getValues("serverId"),
})
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test Connection
</Button>
</div>
) : (
<Button
isLoading={isLoadingConnection}
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
}) })
.catch(() => { .then(async () => {
toast.error("Error to connect the provider"); toast.success("Connection Success");
}); })
}} .catch(() => {
> toast.error("Error to connect the provider");
Test connection });
</Button> }}
>
Test connection
</Button>
)}
<Button <Button
isLoading={form.formState.isSubmitting}
form="hook-form" form="hook-form"
type="submit" type="submit"
isLoading={form.formState.isSubmitting}
> >
Update Update
</Button> </Button>

View File

@@ -11,13 +11,11 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { format } from "date-fns"; import { format } from "date-fns";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const AddGithubProvider = () => { export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data } = api.auth.get.useQuery(); const { data } = api.auth.get.useQuery();
const [manifest, setManifest] = useState(""); const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false); const [isOrganization, setIsOrganization] = useState(false);

View File

@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
export const AddNotification = () => { export const AddNotification = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } = const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
api.notification.testSlackConnection.useMutation(); api.notification.testSlackConnection.useMutation();
@@ -660,26 +660,28 @@ export const AddNotification = () => {
)} )}
/> />
<FormField {!isCloud && (
control={form.control} <FormField
name="dokployRestart" control={form.control}
render={({ field }) => ( name="dokployRestart"
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2"> render={({ field }) => (
<div className="space-y-0.5"> <FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<FormLabel>Dokploy Restart</FormLabel> <div className="space-y-0.5">
<FormDescription> <FormLabel>Dokploy Restart</FormLabel>
Trigger the action when a dokploy is restarted. <FormDescription>
</FormDescription> Trigger the action when a dokploy is restarted.
</div> </FormDescription>
<FormControl> </div>
<Switch <FormControl>
checked={field.value} <Switch
onCheckedChange={field.onChange} checked={field.value}
/> onCheckedChange={field.onChange}
</FormControl> />
</FormItem> </FormControl>
)} </FormItem>
/> )}
/>
)}
</div> </div>
</div> </div>
</form> </form>

View File

@@ -34,7 +34,7 @@ export const ShowNotifications = () => {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<BellRing className="size-8 self-center text-muted-foreground" /> <BellRing className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
To send notifications is required to set at least 1 provider. To send notifications it is required to set at least 1 provider.
</span> </span>
<AddNotification /> <AddNotification />
</div> </div>

View File

@@ -63,7 +63,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
const telegramMutation = api.notification.updateTelegram.useMutation(); const telegramMutation = api.notification.updateTelegram.useMutation();
const discordMutation = api.notification.updateDiscord.useMutation(); const discordMutation = api.notification.updateDiscord.useMutation();
const emailMutation = api.notification.updateEmail.useMutation(); const emailMutation = api.notification.updateEmail.useMutation();
const { data: isCloud } = api.settings.isCloud.useQuery();
const form = useForm<NotificationSchema>({ const form = useForm<NotificationSchema>({
defaultValues: { defaultValues: {
type: "slack", type: "slack",
@@ -618,27 +618,29 @@ export const UpdateNotification = ({ notificationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField {!isCloud && (
control={form.control} <FormField
defaultValue={form.control._defaultValues.dokployRestart} control={form.control}
name="dokployRestart" defaultValue={form.control._defaultValues.dokployRestart}
render={({ field }) => ( name="dokployRestart"
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2"> render={({ field }) => (
<div className="space-y-0.5"> <FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<FormLabel>Dokploy Restart</FormLabel> <div className="space-y-0.5">
<FormDescription> <FormLabel>Dokploy Restart</FormLabel>
Trigger the action when a dokploy is restarted. <FormDescription>
</FormDescription> Trigger the action when a dokploy is restarted.
</div> </FormDescription>
<FormControl> </div>
<Switch <FormControl>
checked={field.value} <Switch
onCheckedChange={field.onChange} checked={field.value}
/> onCheckedChange={field.onChange}
</FormControl> />
</FormItem> </FormControl>
)} </FormItem>
/> )}
/>
)}
</div> </div>
</div> </div>
</form> </form>

View File

@@ -95,7 +95,7 @@ export const ProfileForm = () => {
<div> <div>
<CardTitle className="text-xl">Account</CardTitle> <CardTitle className="text-xl">Account</CardTitle>
<CardDescription> <CardDescription>
Change your details of your profile here. Change the details of your profile here.
</CardDescription> </CardDescription>
</div> </div>
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />} {!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
@@ -145,7 +145,6 @@ export const ProfileForm = () => {
<FormControl> <FormControl>
<RadioGroup <RadioGroup
onValueChange={(e) => { onValueChange={(e) => {
console.log(e);
field.onChange(e); field.onChange(e);
}} }}
defaultValue={field.value} defaultValue={field.value}

View File

@@ -31,6 +31,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -57,6 +58,9 @@ type Schema = z.infer<typeof Schema>;
export const AddServer = () => { export const AddServer = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { data: canCreateMoreServers, refetch } =
api.stripe.canCreateMoreServers.useQuery();
const { data: sshKeys } = api.sshKey.all.useQuery(); const { data: sshKeys } = api.sshKey.all.useQuery();
const { mutateAsync, error, isError } = api.server.create.useMutation(); const { mutateAsync, error, isError } = api.server.create.useMutation();
const form = useForm<Schema>({ const form = useForm<Schema>({
@@ -82,6 +86,10 @@ export const AddServer = () => {
}); });
}, [form, form.reset, form.formState.isSubmitSuccessful]); }, [form, form.reset, form.formState.isSubmitSuccessful]);
useEffect(() => {
refetch();
}, [isOpen]);
const onSubmit = async (data: Schema) => { const onSubmit = async (data: Schema) => {
await mutateAsync({ await mutateAsync({
name: data.name, name: data.name,
@@ -116,6 +124,14 @@ export const AddServer = () => {
Add a server to deploy your applications remotely. Add a server to deploy your applications remotely.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{!canCreateMoreServers && (
<AlertBlock type="warning">
You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan
</Link>
</AlertBlock>
)}
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>} {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}> <Form {...form}>
<form <form
@@ -254,6 +270,7 @@ export const AddServer = () => {
<DialogFooter> <DialogFooter>
<Button <Button
isLoading={form.formState.isSubmitting} isLoading={form.formState.isSubmitting}
disabled={!canCreateMoreServers}
form="hook-form-add-server" form="hook-form-add-server"
type="submit" type="submit"
> >

View File

@@ -19,7 +19,6 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";

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