Compare commits

...

369 Commits

Author SHA1 Message Date
Mauricio Siu 9d988c9a9b Update package.json 2025-02-03 21:49:20 -06:00
Mauricio Siu eb211b933e Merge pull request #1277 from Blueshadow58/revert-1259-pocketbase
revert "feat<templates>: Updated PocketBase version to 0.25.0" #1259
2025-02-03 21:47:59 -06:00
Franco Gamonal 20eb6d7985 Revert "feat<templates>: Updated PocketBase version to 0.25.0" 2025-02-03 10:27:35 -03:00
Mauricio Siu 97b77e526d Merge pull request #1274 from Dokploy/fix/compose-builder
fix: add missing command
2025-02-02 22:19:05 -06:00
Mauricio Siu 077f47f2d8 chore: bump version 2025-02-02 22:18:47 -06:00
Mauricio Siu 5d6847b970 fix: add missing command 2025-02-02 22:18:21 -06:00
Mauricio Siu 2630a73bd8 Merge pull request #1268 from mohabgabber/canary
chore: update unsend image version to v1.2.5
2025-02-02 20:26:24 -06:00
Mauricio Siu df5ef4a34e Merge pull request #1269 from nktnet1/superset-unofficial-template
fix(template): superset SQLALCHEMY_EXAMPLES_URI env + warning
2025-02-02 20:26:11 -06:00
Mauricio Siu 48a8c6021d chore: bump version 2025-02-02 20:25:45 -06:00
Mauricio Siu d84a22fa72 feat: add monitoring arm-amd version 2025-02-02 20:23:49 -06:00
Mauricio Siu 1661022d56 Fix/monitoring (#1271)
* refactor: make request to dokploy server to proxy requests

* refactor: lint

* refactor: use dokploy/monitoring tag image

* refactor: use canary in development or canary tags

* refactor: adjust logic

* refactor: update name
2025-02-02 19:51:19 -06:00
Mauricio Siu 2a295d6492 Merge branch 'canary' into fix/monitoring 2025-02-02 19:50:28 -06:00
Mauricio Siu 51851567db refactor: update name 2025-02-02 19:49:30 -06:00
Mauricio Siu d1aaeb9a7b refactor: adjust logic 2025-02-02 19:43:28 -06:00
Mauricio Siu f9b4035c20 refactor: use canary in development or canary tags 2025-02-02 19:41:07 -06:00
Mauricio Siu d492ff87f2 Fix/monitoring (#1270)
* refactor: make request to dokploy server to proxy requests

* refactor: lint

* refactor: use dokploy/monitoring tag image
2025-02-02 19:35:36 -06:00
Mauricio Siu f638f49ab6 refactor: use dokploy/monitoring tag image 2025-02-02 19:33:52 -06:00
Mauricio Siu d1610855bb refactor: lint 2025-02-02 19:32:45 -06:00
Mauricio Siu 0c8c0844b1 refactor: make request to dokploy server to proxy requests 2025-02-02 19:32:37 -06:00
Tam Nguyen 98b19bb433 chore(template): resolve conflicts with main 2025-02-03 11:54:09 +11:00
Tam Nguyen d8c5244d19 fix(template): superset SQLALCHEMY_EXAMPLES_URI env + warning 2025-02-03 11:47:57 +11:00
Mohab Gabber 7bb8456cb7 chore: update unsend image version to v1.2.5 2025-02-03 02:13:44 +02:00
Mauricio Siu 74a0f5e992 Feat/monitoring (#1267) Cloud Version
* feat: add start monitoring remote servers

* reafctor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor:

* refactor: add metrics

* feat: add disk monitoring

* refactor: translate to english

* refacotor: add stats

* refactor: remove color

* feat: add log server metrics

* refactor: remove unused deps

* refactor: add origin

* refactor: add logs

* refactor: update

* feat: add series monitoring

* refactor: add system monitoring

* feat: add benchmark to optimize data

* refactor: update fn

* refactor: remove comments

* refactor: update

* refactor: exclude items

* feat: add refresh rate

* feat: add monitoring remote servers

* refactor: update

* refactor: remove unsued volumes

* refactor: update monitoring

* refactor: add more presets

* feat: add container metrics

* feat: add docker monitoring

* refactor: update conversion

* refactor: remove unused code

* refactor: update

* refactor: add docker compose logs

* refactor: add docker cli

* refactor: add install curl

* refactor: add get update

* refactor: add monitoring remote servers

* refactor: add containers config

* feat: add container specification

* refactor: update path

* refactor: add server filter

* refactor: simplify logic

* fix: verify if file exist before get stats

* refactor: update

* refactor: remove unused deps

* test: add test for containers

* refactor: update

* refactor add memory collector

* refactor: update

* refactor: update

* refactor: update

* refactor: remove

* refactor: add memory

* refactor: add server memory usage

* refactor: change memory

* refactor: update

* refactor: update

* refactor: add container metrics

* refactor: comment code

* refactor: mount proc bind

* refactor: change interval with node cron

* refactor: remove opening file

* refactor: use streams

* refactor: remove unused ws

* refactor: disable live when is all

* refactor: add sqlite

* refactor: update

* feat: add golang benchmark

* refactor: update go

* refactor: update dockerfile

* refactor: update db

* refactor: add env

* refactor: separate logic

* refactor: split logic

* refactor: update logs

* refactor: update dockerfile

* refactor: hide .env

* refactor: update

* chore: hide ,.ebnv

* refactor: add end angle

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update monitoring

* refactor: add mount db

* refactor: add metrics and url callback

* refactor: add middleware

* refactor: add threshold property

* feat: add memory and cpu threshold notification

* feat: send notifications to the server

* feat: add metrics for dokploy server

* refactor: add dokploy server to monitoring

* refactor: update methods

* refactor: add admin to useeffect

* refactor: stop monitoring containers if elements are 0

* refactor: cancel request if appName is empty

* refactor: reuse methods

* chore; add feat monitoring

* refactor: set base url

* refactor: adjust monitoring

* refactor: delete migrations

* feat: add columns

* fix: add missing flag

* refactor: add free metrics

* refactor: add paid monitoring

* refactor: update methods

* feat: improve ui

* feat: add container stats

* refactor: add all container metrics

* refactor: add color primary

* refactor: change default rate limiting refresher

* refactor: update retention days

* refactor: use json instead of individual properties

* refactor: lint

* refactor: pass json env

* refactor: update

* refactor: delete

* refactor: update

* refactor: fix types

* refactor: add retention days

* chore: add license

* refactor: create db

* refactor: update path

* refactor: update setup

* refactor: update

* refactor: create files

* refactor: update

* refactor: delete

* refactor: update

* refactor: update token metrics

* fix: typechecks

* refactor: setup web server

* refactor: update error handling and add monitoring

* refactor: add local storage save

* refactor: add spacing

* refactor: update

* refactor: upgrade drizzle

* refactor: delete

* refactor: uppgrade drizzle kit

* refactor: update search with jsonB

* chore: upgrade drizzle

* chore: update packages

* refactor: add missing type

* refactor: add serverType

* refactor: update url

* refactor: update

* refactor: update

* refactor: hide monitoring on self hosted

* refactor: update server

* refactor: update

* refactor: update

* refactor: pin node version
2025-02-02 14:08:06 -06:00
Mauricio Siu 8c69d2a085 Merge pull request #1265 from Dokploy/1199-error-log-spam-in-traefik-logs-err-errorservice-dokploy-traefik-error-port-is-missing
refactor: delete label
2025-02-01 23:05:24 -06:00
Mauricio Siu c8a4a826ca refactor: delete label 2025-02-01 23:04:31 -06:00
Mauricio Siu e1b114a63b Merge pull request #1264 from Dokploy/1202-dates-are-not-shown-in-logs
fix: don't cut log
2025-02-01 23:00:45 -06:00
Mauricio Siu 0b4d19abd6 fix: don't cut log 2025-02-01 23:00:28 -06:00
Mauricio Siu 1c0f6a8e60 Merge pull request #1262 from Dokploy/1235-monitoring-404-not-found---mcpuvalue-is-null
refactor: update docker stats
2025-02-01 21:49:03 -06:00
Mauricio Siu c41aa0ccf7 refactor: uypdate 2025-02-01 19:31:44 -06:00
Mauricio Siu 96bb72eb99 refactor: update docker stats 2025-02-01 19:27:10 -06:00
Mauricio Siu ee2fed07b2 Merge pull request #1252 from mateodemuynck/fix/stack-domains
fix: expose docker stack services
2025-02-01 13:50:19 -06:00
Mauricio Siu af083ffa5d refactor: remove duplicate case 2025-02-01 13:38:24 -06:00
Mauricio Siu f14ed5170d Merge pull request #1249 from ali-issa/fix/domains-page-ux-improvements
fix: improve domains page UX and button placement consistency
2025-02-01 13:32:45 -06:00
Mauricio Siu cd1c7e60bf refactor: update domains 2025-02-01 13:32:10 -06:00
Mauricio Siu 79c6b7389c Merge pull request #1259 from Blueshadow58/pocketbase
feat<templates>: Updated PocketBase version to 0.25.0
2025-02-01 11:20:40 -06:00
Mauricio Siu e48f1431a9 Merge pull request #1238 from wish-oss/feat/copy-ip
feat: added clipboard functionality to copy IP address from badge in services pages
2025-02-01 11:20:11 -06:00
blueshadow58 c2ecdb2d76 feat<templates>: Updated PocketBase version to 0.25.0 2025-02-01 13:25:33 -03:00
Vishal kadam 5c889e81a9 Merge branch 'Dokploy:canary' into feat/copy-ip 2025-02-01 14:12:07 +00:00
Mauricio Siu 407e2e1137 Merge pull request #1250 from MuhammadM1998/patch-1
chore: remove duplicated sentence in projects dialog
2025-02-01 02:41:21 -06:00
vishalkadam47 2c6c89e4c1 refactor: added copy-to-clipboard library and replaced navigator.clipboard.writeText with copy function 2025-01-31 20:31:16 +05:30
Vishal kadam 41a8014585 Merge branch 'Dokploy:canary' into feat/copy-ip 2025-01-31 14:12:31 +00:00
Motysten fffe1d6249 fix: labels not added correctly when using "stack" type docker dompose file 2025-01-31 14:15:52 +01:00
Muhammad Mahmoud cf0f5c8b97 chore: remove duplicated sentence 2025-01-31 13:55:18 +02:00
Ali Issa 777980618f fix: improve domains page UX and button placement consistency 2025-01-31 02:59:23 -05:00
Mauricio Siu dcd1df31c7 Merge pull request #637 from xenonwellz/feat/stack-env-support
Feat: added env support to Dokploy stack compose
2025-01-31 01:38:29 -06:00
Mauricio Siu 7369b54f32 refactor: update 2025-01-31 01:20:52 -06:00
Mauricio Siu 009859faa9 refactor: add .env docker stack 2025-01-31 01:20:10 -06:00
Mauricio Siu f7a29accb1 refactor: lint 2025-01-30 23:40:01 -06:00
Mauricio Siu 50c518a63a Merge branch 'canary' into feat/stack-env-support 2025-01-30 23:39:54 -06:00
Mauricio Siu 18c6d08b9a Merge pull request #1237 from bernabedev/canary
feat(template): add Formbricks template with Docker Compose and environment setup
2025-01-30 23:32:32 -06:00
Mauricio Siu 208094cd3e Update apps/dokploy/templates/formbricks/index.ts 2025-01-30 23:31:43 -06:00
Mauricio Siu 1342f73a02 Update apps/dokploy/templates/formbricks/index.ts 2025-01-30 23:31:39 -06:00
Mauricio Siu 1787c524f0 Update apps/dokploy/templates/formbricks/docker-compose.yml 2025-01-30 23:31:33 -06:00
Mauricio Siu 5899dc9394 Merge pull request #1114 from joaotonaco/feat/template-evolution-api
feat(template): added evolution api
2025-01-30 23:24:25 -06:00
Mauricio Siu 6b48c0f354 fix: correct env values 2025-01-30 23:24:12 -06:00
Mauricio Siu 33f3d1d87e Merge branch 'canary' into feat/template-evolution-api 2025-01-30 23:16:09 -06:00
Mauricio Siu 4bfb172373 Merge pull request #1232 from muhammetakalan/patch-1
fix: teable template
2025-01-30 23:11:34 -06:00
Mauricio Siu 6cf96df6ec Merge pull request #1231 from jannismilz/canary
feat: upgrade listmonk version
2025-01-30 23:09:01 -06:00
Freilyn Bernabe 00d2b3b572 fix: changing the description of formbricks to english 2025-01-29 07:26:14 -04:00
vishalkadam47 d8f1548076 feat: added clipboard functionality to copy IP address from badge in services pages 2025-01-29 13:09:58 +05:30
Freilyn Bernabe de4d1c0911 feat(template): add Formbricks template with Docker Compose and environment setup 2025-01-29 00:39:01 -04:00
Muhammet Emin Akalan b96169fa55 fix: teable template 2025-01-28 15:31:20 +03:00
Jannis Milz e21e0e1865 feat: Upgrade listmonk version and remove automatic admin credentials creation 2025-01-28 12:24:54 +01:00
Mauricio Siu dc9a194bbe Merge pull request #1229 from nktnet1/shlink-template
fix(template): shlink version stable in index.ts
2025-01-27 22:57:48 -06:00
Khiet Tam Nguyen 27738d253e fix(template): shlink version stable in index.ts 2025-01-28 01:47:14 +11:00
Mauricio Siu d37bde00bc Merge pull request #1226 from nktnet1/shlink-template
feat(template): added shlink, a url shortener service
2025-01-26 23:44:13 -06:00
Khiet Tam Nguyen 55fae23ce3 feat(template): added shlink, a url shortener service 2025-01-27 14:36:07 +11:00
Mauricio Siu d43cd52762 refactor: update 2025-01-26 19:58:35 -06:00
Mauricio Siu 094491ecbf refactor: add missing deps 2025-01-26 19:47:26 -06:00
Mauricio Siu 3d5bed0915 chore: add missing dependency 2025-01-26 19:30:21 -06:00
Mauricio Siu 23de14f0b4 chore: bump version 2025-01-26 19:15:11 -06:00
Mauricio Siu 215811ae82 Merge pull request #1224 from Dokploy/1093-expose-deploy-preview-url-in-env
feat: add DOKPLOY_DEPLOY_URL in enviroment variables for previews
2025-01-26 19:14:01 -06:00
Mauricio Siu 34b4956630 feat: add DOKPLOY_DEPLOY_URL in enviroment variables for previews 2025-01-26 19:13:43 -06:00
Mauricio Siu 7ed0282ce1 Merge pull request #1223 from Dokploy/fix/remove-cron-jobs-after-delete
Fix/remove cron jobs after delete
2025-01-26 19:01:32 -06:00
Mauricio Siu 24327139b8 refactor: make optional field name 2025-01-26 19:00:58 -06:00
Mauricio Siu 0fb67ced5d fix: remove cron jobs after delete service 2025-01-26 18:59:27 -06:00
Mauricio Siu 1e56364f93 refactor: update typo 2025-01-26 18:26:49 -06:00
Mauricio Siu f980e459d9 Merge pull request #1220 from bernabedev/canary
feat(template): added answer template
2025-01-26 17:57:40 -06:00
Mauricio Siu 3209550edc Update apps/dokploy/templates/answer/docker-compose.yml 2025-01-26 17:57:31 -06:00
Mauricio Siu 9835ead2b9 Merge pull request #1222 from Dokploy/1164-when-creating-a-service-and-selecting-a-server-display-description-information
refactor: add server IP address
2025-01-26 17:50:07 -06:00
Mauricio Siu 73f93f8a13 refactor: add server IP address 2025-01-26 17:49:51 -06:00
Mauricio Siu 46ddaa68fa Merge pull request #1221 from Dokploy/1170-0176-copy-to-clipboard-stopped-working-on-the-project-delete-confirm-dialog
refactor: add back delete with confirmation
2025-01-26 17:45:03 -06:00
Mauricio Siu 64e68cfde1 refactor: update styles 2025-01-26 17:44:31 -06:00
Mauricio Siu 36c1a615c6 refactor: use copy package 2025-01-26 17:42:04 -06:00
Mauricio Siu eeb97645b5 refactor: add back delete with confirmation 2025-01-26 17:40:28 -06:00
Freilyn Bernabe 9a052c4657 Merge branch 'Dokploy:canary' into canary 2025-01-26 19:25:44 -04:00
Freilyn Bernabe 165cdd27da feat(template): added answer template 2025-01-26 19:22:17 -04:00
Mauricio Siu 13551f6065 Merge pull request #1219 from Dokploy/706-ability-to-use-a-custom-port-for-git
fix: set extracted port on ssh connection
2025-01-26 17:21:09 -06:00
Mauricio Siu f2ad1c5a57 fix: set extracted port on ssh connection 2025-01-26 17:20:33 -06:00
Mauricio Siu 4d5565895c Merge pull request #1218 from Dokploy/1059-inconsistence-layout-resource
refactor: adjust order
2025-01-26 16:28:17 -06:00
Mauricio Siu 8e51dedb78 refactor: adjust order 2025-01-26 16:28:00 -06:00
Mauricio Siu 780fa6b9cd Merge pull request #1217 from Dokploy/1177-when-deploying-applications-through-autodeploy-docker-cannot-update
refactor: make less aggressive cleanups
2025-01-26 16:25:16 -06:00
Mauricio Siu 95e642e63a refactor: make less aggressive cleanups 2025-01-26 16:20:02 -06:00
Mauricio Siu 5801c69034 Merge pull request #1216 from Dokploy/1190-cant-reach-my-app-by-assigned-domain
refactor: add dokploy-network on new installations to prevent gateway…
2025-01-26 15:54:47 -06:00
Mauricio Siu 2c98b4267f refactor: add dokploy-network on new installations to prevent gateway 504 errors randomly 2025-01-26 15:53:50 -06:00
Mauricio Siu 1874ffaa55 Merge pull request #1215 from Dokploy/1210-search-repository-function-is-not-working-correctly
fix: search by name and not on url
2025-01-26 15:46:11 -06:00
Mauricio Siu 11c4101dc3 fix: search by name and not on url 2025-01-26 15:45:53 -06:00
Mauricio Siu 4c3b5ef271 Merge pull request #1214 from NagariaHussain/template-erpnext
feat: add erpnext template
2025-01-26 15:29:58 -06:00
Mauricio Siu 4105353109 Merge branch 'canary' into template-erpnext 2025-01-26 15:20:30 -06:00
Mauricio Siu 8a971072e4 Merge pull request #1205 from nktnet1/superset-avoid-host-conflict
fix(template): prepend superset_ to postgres/redis
2025-01-26 15:19:46 -06:00
Mauricio Siu dab12d6162 Merge pull request #1204 from gentslava/fix/issue_1198-muliple_preview_deployments
fix: Multiple Preview Deployments
2025-01-26 15:19:00 -06:00
Mauricio Siu 8d31574e5f refactor: add continue to is cloud 2025-01-26 15:17:26 -06:00
Mauricio Siu 0012ca7357 Merge pull request #1203 from wish-oss/fix/xterm
fix: terminal paste visibility
2025-01-26 15:02:26 -06:00
Mauricio Siu 1aed53e6fe refactor: add missing load 2025-01-26 15:01:49 -06:00
Mauricio Siu d6b966cfea refactor: merge canary 2025-01-26 15:01:30 -06:00
Mauricio Siu 772341fb1e refactor: remove unused settings 2025-01-26 14:57:33 -06:00
Mauricio Siu e7a6247297 refactor: lint 2025-01-26 14:56:01 -06:00
Mauricio Siu b1beb7b71b fix: adjust color paste on terminal 2025-01-26 14:55:22 -06:00
Mauricio Siu 6254644fa6 Merge pull request #1196 from rahadi23/bugfix/1165-navigation-items-permissions
fix(ui): add condition to show update server button to admin only
2025-01-26 14:30:39 -06:00
Mauricio Siu bcb86f3d6d Merge pull request #1195 from bernabedev/canary
feat(template): added maybe finance
2025-01-26 14:30:17 -06:00
Mauricio Siu b96103247a Merge branch 'canary' into canary 2025-01-26 14:30:11 -06:00
Mauricio Siu 87697147da Update apps/dokploy/templates/maybe/docker-compose.yml 2025-01-26 14:29:51 -06:00
Mauricio Siu 074e3b6ec6 Merge pull request #1194 from wish-oss/feat/spacedrive-template
feat: add spacedrive file manager template
2025-01-26 14:24:52 -06:00
Mauricio Siu 7061e06736 Merge branch 'canary' into feat/spacedrive-template 2025-01-26 14:24:46 -06:00
Mauricio Siu de35812d5a Merge pull request #1193 from wish-oss/feat/domain-navigation
feat: restore domain dropdown menu functionality
2025-01-26 14:22:34 -06:00
Mauricio Siu c5ac5f84b1 Merge pull request #1186 from gentslava/canary
feat(i18n): add missing keys to Russian translation
2025-01-26 14:21:20 -06:00
Mauricio Siu 2e42fa7014 Merge pull request #1185 from RoodyCode/patch-1
style: fix incosistent spacing in Card-Footer
2025-01-26 14:21:05 -06:00
Mauricio Siu 0fe8a1a221 Merge pull request #1183 from yni9ht/feat-alist-template
feat(template): add alist template
2025-01-26 14:20:15 -06:00
Hussain Nagaria 829e77a6b8 feat: add erpnext template
Co-authored-by: Revant <revant.one@gmail.com>
2025-01-26 21:50:10 +05:30
Khiet Tam Nguyen adfe598671 fix(template): prepend superset_ to postgres/redis 2025-01-25 19:14:58 +11:00
Vyacheslav Shcherbinin ac49235916 Pull Request actions filter 2025-01-25 10:51:22 +07:00
vishalkadam47 d7210e9d7b fix: terminal paste visibility 2025-01-25 00:42:46 +05:30
Rahadi Jalu 52ea9ffa67 fix(ui): add condition to show update server button to admin only 2025-01-24 15:05:02 +07:00
Freilyn Bernabe fd570ff861 feat(template): added maybe finance 2025-01-23 23:41:58 -04:00
vishalkadam47 130567dd78 feat: add Spacedrive file manager template 2025-01-24 08:17:39 +05:30
vishalkadam47 0a826fbf1c feat: add dropdown menu for service domains and add download functionality for SSH keys 2025-01-24 06:13:54 +05:30
Vyacheslav Shcherbinin 081d08c20a Update ru translation 2025-01-23 20:20:59 +07:00
Roody 41cf3d7b77 style: fix incosistent spacing 2025-01-23 11:32:08 +01:00
yni9ht b05af62b7b feat: add alist template 2025-01-23 14:58:45 +08:00
Mauricio Siu c6569f70e4 Merge pull request #1182 from Dokploy/fix/disable-turn-off-cleanup
fix: turnoff cleanup cache
2025-01-23 00:54:30 -06:00
Mauricio Siu d5c9338f51 fix: turnoff cleanup cache 2025-01-23 00:53:21 -06:00
Mauricio Siu dba39b6364 Merge pull request #1180 from Dokploy/feat/add-error-logs-remote-server
Feat/add error logs remote server
2025-01-23 00:50:35 -06:00
Mauricio Siu 22de0fef49 chore: bump version 2025-01-23 00:44:48 -06:00
Mauricio Siu 03e1c17675 feat: add remote logs error when is not reachable 2025-01-23 00:44:31 -06:00
Mauricio Siu 6edd2a81e5 refactor: lint 2025-01-22 23:45:03 -06:00
Mauricio Siu fe5b8782e9 chore: bump version 2025-01-22 23:44:43 -06:00
Mauricio Siu 71f28fae70 Merge pull request #1176 from wish-oss/feat/refactor-templates
feat:update templates glance and homarr
2025-01-22 23:43:53 -06:00
Mauricio Siu c44618aa95 Merge pull request #1169 from SHABIN-K/add-malayalam-language
style(i18n) add malayalamlanguage
2025-01-22 23:39:37 -06:00
Mauricio Siu c7d86dd430 Merge pull request #1178 from Dokploy/1175-preview-deployment-not-found-correctly
fix: set right branch in preview remote deployments
2025-01-22 23:33:29 -06:00
Mauricio Siu e50bbd1a6a fix: set right branch in preview remote deployments 2025-01-22 23:33:05 -06:00
vishalkadam47 d5ff91563a fix: docker-compose.yaml to docker-compose.yml 2025-01-23 10:08:50 +05:30
vishalkadam47 210fe0759c feat:update templates glance and homarr 2025-01-23 03:43:04 +05:30
Shabin k edff66900e add malayalam support 2025-01-22 12:26:05 +05:30
Shabin k 4cf2177928 add malayalam support 2025-01-22 12:23:38 +05:30
Shabin k 4a8306b015 Merge branch 'Dokploy:canary' into add-malayalam-language 2025-01-22 12:22:54 +05:30
Shabin k f92d6693c3 add malayalam support 2025-01-22 12:22:27 +05:30
Mauricio Siu 81248ed03f fix: add continue to process all applications 2025-01-22 00:39:28 -06:00
Mauricio Siu c7d5900e11 chore: bump version 2025-01-22 00:25:25 -06:00
Mauricio Siu d0608f43a9 Merge pull request #1166 from rahadi23/bugfix/1165-navigation-items-permissions
fix: filter navigation items based on user's permissions and role
2025-01-22 00:24:55 -06:00
Mauricio Siu adaf12a9a4 refactor: update 2025-01-22 00:16:00 -06:00
Mauricio Siu baa2ca20f4 Merge branch 'canary' into bugfix/1165-navigation-items-permissions 2025-01-22 00:15:31 -06:00
Mauricio Siu 537caf02e5 Merge pull request #1167 from Dokploy/fix/creation-users
refactor: make protected instead of admin
2025-01-22 00:14:49 -06:00
Mauricio Siu 02ff507094 refactor: update lint 2025-01-22 00:14:30 -06:00
Mauricio Siu 53df7d969e refactor: make protected instead of admin 2025-01-22 00:13:22 -06:00
Rahadi Jalu 9e6e68558a fix: adjust dialog title based on add/update condition 2025-01-22 11:26:18 +07:00
Rahadi Jalu a2e9ea2986 fix: add condition to show create project button 2025-01-22 11:26:18 +07:00
Rahadi Jalu 026e1bece6 fix: filter navigation items based on user's permissions and role 2025-01-22 11:26:18 +07:00
Mauricio Siu 51f6e08e16 Merge pull request #1158 from nktnet1/superset-unofficial-template
feat(template): added apache superset (unofficial)
2025-01-21 21:59:04 -06:00
Mauricio Siu c0f8218ad9 Merge pull request #1161 from nktnet1/volume-file-mount-content-line-clamp
fix(ui): volume file mount content, line clamp and preserve whitespace
2025-01-21 21:51:07 -06:00
Mauricio Siu 35dd6bff7a Merge pull request #1160 from nktnet1/fix-invisible-mount-path
fix(ui): volume mountPath undefined for file mount - meant to display filePath instead?
2025-01-21 21:50:33 -06:00
Mauricio Siu 8dad8fd008 Merge pull request #1162 from mikield/add-ukrainian-language
style(i18n) add ukrainian language
2025-01-21 21:31:33 -06:00
Tam Nguyen 52dbc0d8f1 fix(template): superset healthchecks 2025-01-22 09:14:21 +11:00
Vladyslav G 692f883064 style(i18n) add ukrainian language 2025-01-21 16:21:29 +01:00
Khiet Tam Nguyen 6d90e268f7 fix(ui): volume file mount content, line clamp and preserve whitespace 2025-01-22 01:09:44 +11:00
Khiet Tam Nguyen 1d86f1a0fc fix(template): added analytics tag to superset 2025-01-22 00:05:55 +11:00
Khiet Tam Nguyen c7338983b8 refactor(ui): clearer ui display condition for volume mount display 2025-01-21 23:45:54 +11:00
Khiet Tam Nguyen ce06cd42b3 fix(ui): show filePath instead of mountPath for file mounts 2025-01-21 23:41:05 +11:00
Khiet Tam Nguyen 1a44a0ea5a refactor(template): use dokploy mount volume for superset_config.py 2025-01-21 23:14:28 +11:00
Khiet Tam Nguyen 444121f8d8 fix(template): more appropriate tags for superset 2025-01-21 20:40:46 +11:00
Khiet Tam Nguyen 05a75edbec feat(template): added apache superset (unofficial) 2025-01-21 19:03:35 +11:00
Mauricio Siu f5d81f434c chore: bump package 2025-01-19 13:25:44 -06:00
Mauricio Siu 10353d1f29 Merge pull request #1147 from eltociear/patch-2
chore(logs): update utils.ts
2025-01-19 13:24:54 -06:00
Mauricio Siu 6226c75959 Merge pull request #1148 from Dokploy/fix/clean-cache
Fix/clean cache
2025-01-19 13:24:37 -06:00
Mauricio Siu 42c9cd5901 chore: bump package 2025-01-19 13:24:22 -06:00
Mauricio Siu 49edcdb99e fix: clean cache at the start of deployments 2025-01-19 13:22:23 -06:00
Ikko Eltociear Ashimine ad71e8b36a chore(logs): update utils.ts
Exemple -> Example
2025-01-20 03:15:02 +09:00
Mauricio Siu b166cd5bfa refactor: add settings page to user nav 2025-01-19 11:30:44 -06:00
Mauricio Siu 0045608acc chore: remove release title 2025-01-19 11:22:31 -06:00
Mauricio Siu b140e81210 chore: update release 2025-01-19 11:20:22 -06:00
Mauricio Siu 9298d6c693 Merge pull request #1144 from Dokploy/canary
🚀 Release v0.17.3
2025-01-19 11:11:36 -06:00
Mauricio Siu 23df3fba85 refactor: regenerate migrations 2025-01-19 11:01:41 -06:00
Mauricio Siu 93c7f1ce76 chore: update 2025-01-19 10:56:43 -06:00
Mauricio Siu 1323394589 chore: remove draft 2025-01-19 10:51:05 -06:00
Mauricio Siu 4f11fc2547 chore: remove spaces 2025-01-19 10:49:08 -06:00
Mauricio Siu 6d052ad455 chore: add quotes 2025-01-19 10:47:04 -06:00
Mauricio Siu 60748da144 chore: update 2025-01-19 10:45:51 -06:00
Mauricio Siu 1956836cde refactor: update 2025-01-19 10:45:12 -06:00
Mauricio Siu 7dca4fe430 refactor: update 2025-01-19 10:43:32 -06:00
Mauricio Siu 84690c5f75 chore: update pr 2025-01-19 10:41:44 -06:00
Mauricio Siu 95b67ef2e9 chore: update 2025-01-19 10:36:47 -06:00
Mauricio Siu 3f8bc47ce5 refactor: update 2025-01-19 10:35:07 -06:00
Mauricio Siu 498678c4ae Revert "refactor: update"
This reverts commit 3d602c232d.
2025-01-19 10:21:30 -06:00
Mauricio Siu 3d602c232d refactor: update 2025-01-19 10:19:59 -06:00
Mauricio Siu 94ffa7d578 chore: update 2025-01-19 10:15:28 -06:00
Mauricio Siu 64fc3c7677 chore: remove pr 2025-01-19 10:14:58 -06:00
Mauricio Siu f27830daf0 refactor: update version 2025-01-19 10:08:53 -06:00
Mauricio Siu 3ec2e2dd1a Revert "chore: bump version"
This reverts commit 1e006cb094.
2025-01-19 10:06:50 -06:00
Mauricio Siu 1e006cb094 chore: bump version 2025-01-19 10:00:54 -06:00
Mauricio Siu 8aa655af2c chore: add pr 2025-01-19 10:00:09 -06:00
Mauricio Siu 65659e27f1 Merge pull request #1137 from Dokploy/feat/github-runners
Feat/GitHub runners
2025-01-19 09:58:27 -06:00
Mauricio Siu 4b6f9108d4 refactor: update 2025-01-19 02:56:08 -06:00
Mauricio Siu 1e4a41a8e3 chore: replace circle with github actions 2025-01-19 02:55:05 -06:00
Mauricio Siu 539aa7a85b refactor: print version 2025-01-19 02:47:14 -06:00
Mauricio Siu b6ae502b92 refactor: update 2025-01-19 02:38:38 -06:00
Mauricio Siu e82db47ec4 refactor: add github docker 2025-01-19 02:33:53 -06:00
Mauricio Siu f9b4f008c2 Merge pull request #1134 from Dokploy/1031-excessive-unused-docker-cache-generated-by-dokploy-deployments
feat: add cleanup cache on deployments
2025-01-19 02:08:48 -06:00
Mauricio Siu adb204ec1f refactor: add sidebar persistence 2025-01-19 01:44:42 -06:00
Mauricio Siu 5310a559b0 refactor: improve sidebar 2025-01-19 01:29:29 -06:00
Mauricio Siu 43b7db00f9 refactor: add missing values to test 2025-01-19 01:21:03 -06:00
Mauricio Siu 52c83fd6fc refactor: update text 2025-01-19 01:07:41 -06:00
Mauricio Siu 25a8df567e feat: add cleanup cache on deployments 2025-01-19 00:57:42 -06:00
Mauricio Siu 089274492d Merge pull request #1125 from SlavenIvanov/feat/add-couch-db-template
feat: added couchdb template
2025-01-18 17:39:12 -06:00
Mauricio Siu 65c0ea829f Merge branch 'canary' into feat/add-couch-db-template 2025-01-18 17:39:02 -06:00
Mauricio Siu d060eec465 Update apps/dokploy/templates/couchdb/docker-compose.yml 2025-01-18 17:38:33 -06:00
Mauricio Siu 2dca0d343e Merge pull request #1069 from nktnet1/accessed-typo
chore: typo "accesed" changed to "accessed" for TS code
2025-01-18 17:35:04 -06:00
Mauricio Siu a8f63bb4d3 Merge pull request #1131 from agustints/fix/db-backups-destination-url
fix: Update link text and destination for db backup settings
2025-01-18 17:34:09 -06:00
Mauricio Siu cecd371988 Merge pull request #1081 from depado/gotify-notifications
feat(notifications): implement gotify provider
2025-01-18 17:33:55 -06:00
Mauricio Siu 6c4d94cb4f Merge pull request #1118 from izayl/feature/ci-skip
feat: add ci skip check
2025-01-18 17:30:41 -06:00
Mauricio Siu c4e5c818f3 Merge branch 'canary' into gotify-notifications 2025-01-18 17:23:35 -06:00
Mauricio Siu 31a35d91e8 Merge pull request #1088 from shiqocred/canary
Update telegram message notification
2025-01-18 17:22:50 -06:00
Mauricio Siu 74a2f79a36 Merge pull request #1123 from nktnet1/it-tools-template
feat(template): added it-tools
2025-01-18 11:02:42 -06:00
Mauricio Siu a8f8a727bd Merge pull request #1128 from TheLetslook/patch-1
feat(style): custom scrollbar styling
2025-01-18 11:00:42 -06:00
Mauricio Siu e8f2ab35c9 Merge pull request #1121 from thebadking/canary
style: fix tablet and mobile (Create from Template)
2025-01-18 10:52:24 -06:00
depado 9806a5d607 feat(notifications): fix gotify style 2025-01-18 14:23:53 +01:00
depado e25d0c0c68 feat(notifications): implement notifications for gotify 2025-01-18 14:23:27 +01:00
depado 1f8a476264 chore(lint): run biome 2025-01-18 14:23:27 +01:00
depado cc473b3e87 feat(notifications): implement gotify provider 2025-01-18 14:23:27 +01:00
Agustin Tornielli 10b3543d39 fix: Update link text and destination for db backup settings 2025-01-18 01:19:33 -03:00
thebadking 0893149db0 style: grid fix 2025-01-17 20:45:17 +00:00
Vasiliy e257f86194 feat(style): custom scrollbar styling 2025-01-17 21:49:44 +03:00
Slaven Ivanov a9a0b4cb03 feat: added couchdb template 2025-01-17 18:09:02 +02:00
Khiet Tam Nguyen c5073c9f30 feat(template): added it-tools 2025-01-18 01:02:39 +11:00
João Gabriel 34ab01fcae Merge branch 'canary' into feat/template-evolution-api 2025-01-17 10:38:14 -03:00
João Gabriel df43f8318a fix: evolution api database provider 2025-01-17 10:37:01 -03:00
izayl e69c602d1c chore: enhance deployment skip message to include reason for skipping 2025-01-17 15:47:18 +08:00
izayl 0116d995d9 test: extractCommitMessage 2025-01-17 15:32:59 +08:00
Mauricio Siu ad479c4be1 Merge pull request #1112 from nktnet1/conduwuit-template
feat(template): added conduwuit
2025-01-16 22:24:46 -06:00
Mauricio Siu f70192a71c refactor: lint 2025-01-16 22:24:31 -06:00
Mauricio Siu abff70f081 refactor: lint 2025-01-16 22:24:17 -06:00
Mauricio Siu 9f03faaec2 Merge branch 'canary' into conduwuit-template 2025-01-16 22:24:03 -06:00
Mauricio Siu 1d760bd25f Merge pull request #1104 from nktnet1/cloudflared-template
feat(template): added cloudflared
2025-01-16 22:20:59 -06:00
Mauricio Siu 26a67dd175 Merge pull request #1119 from wish-oss/extrernal-link
refactor: update icon handling and restructure sidebar external links
2025-01-16 22:15:57 -06:00
thebadking 013ee89a56 style: fix tablet and mobile (Create from Template) 2025-01-16 21:12:11 +00:00
vishalkadam47 c3d3c7b96a refactor: update icon handling and restructure sidebar external links 2025-01-16 19:31:55 +05:30
izayl 4e724d88aa feat: add ci skip check 2025-01-16 16:26:28 +08:00
Mauricio Siu 351e4b6103 Merge pull request #1115 from TheLetslook/patch-1
fix: db type typo
2025-01-15 23:30:29 -06:00
Vasiliy 5b633ec6c5 fix: db type typo
Postgres -> Mongo
2025-01-14 23:25:22 +03:00
João Gabriel a46cbf4f2c feat: evolution api template 2025-01-14 08:36:29 -03:00
Khiet Tam Nguyen ca9552d618 chore: sort conduwuit tags 2025-01-14 17:30:45 +11:00
Khiet Tam Nguyen 23b40c1aa7 feat(template): added conduwuit 2025-01-14 17:21:05 +11:00
Mauricio Siu 5e0b7ba143 Merge pull request #1109 from 190km/fix/sidebar
fix: fixed missing cluster side item
2025-01-13 23:50:02 -06:00
Mauricio Siu cb1203e302 refactor: exclude cluster from cloud 2025-01-13 23:49:22 -06:00
Mauricio Siu 373c5bc507 Merge pull request #1106 from DJKnaeckebrot/fix/styling-after-sidebar-fixes
fix(style): clean up sidebar related style issue
2025-01-13 23:41:00 -06:00
Mauricio Siu ee5afa4793 Merge pull request #1110 from szwabodev/fix/wslCheck
fix: isWSL check was not awaited
2025-01-13 22:58:15 -06:00
Mauricio Siu 7b9426586d Merge pull request #1107 from joaotonaco/refactor/improve_sidebar_animation
refactor(sidebar): improve animation
2025-01-13 22:38:41 -06:00
190km 772b24a415 fix: fixed missing cluster side item 2025-01-13 14:12:19 +00:00
UndefinedPony 1922437115 fix: isWSL check was not awaited causing always true 2025-01-13 14:48:13 +01:00
João Gabriel c5eb8b087d refactor: transition logo size 2025-01-13 09:20:02 -03:00
João Gabriel 94ee5391fb refactor: apply ease-out to sidebar toggle animation 2025-01-13 09:19:32 -03:00
djknaeckebrot b3ff14f792 style: clean up class names for consistency in dashboard components 2025-01-13 12:09:06 +01:00
djknaeckebrot b68273c8ca style: adjust grid layout for monitoring card and clean up class names 2025-01-13 12:02:59 +01:00
djknaeckebrot 8b203c48b4 feat: update sidebar with Traefik File System title and add support link 2025-01-13 11:55:58 +01:00
djknaeckebrot fb33a5b6a5 style: make sure card do use the full width of the div 2025-01-13 11:53:05 +01:00
Khiet Tam Nguyen b17ab6d8cb feat(template): added cloudflared 2025-01-13 20:02:18 +11:00
Mauricio Siu 30d20bd267 Merge pull request #1101 from Dokploy/canary
v0.17.2
2025-01-13 02:28:37 -06:00
Mauricio Siu f9b1c2575e refactor: lint 2025-01-13 02:28:00 -06:00
Mauricio Siu b0b22224c3 revert: add missing installation buttons 2025-01-13 02:27:49 -06:00
shiqocred 2de0e73284 Merge branch 'Dokploy:canary' into canary 2025-01-13 13:31:31 +07:00
shiqocred 537950dd9f Revision notification (#7)
* Update build-success.ts

* Update compose.ts

* Update application.ts

* Update notification.ts

* Update utils.ts

* Update dokploy-restart.ts

* Update docker-cleanup.ts

* Update database-backup.ts

* Update build-success.ts

* Update build-success.ts
2025-01-13 13:13:13 +07:00
Mauricio Siu f2f3986c56 Merge pull request #1100 from Dokploy/canary
v0.17.1
2025-01-13 00:00:05 -06:00
Mauricio Siu dd3fccea02 refactor: adjust sizes 2025-01-12 23:59:35 -06:00
Mauricio Siu 5052688aaf Merge pull request #1099 from Dokploy/fix/version
fix: adjust size of cards and add ssh keys
2025-01-12 23:52:58 -06:00
Mauricio Siu 5825b3eae7 Merge pull request #1098 from nktnet1/fix-ssh-keys-create-server
fix: query for ssh keys to show servers, rather than default empty
2025-01-12 23:52:28 -06:00
Mauricio Siu dbca102178 fix: adjust size of cards and add ssh keys 2025-01-12 23:51:57 -06:00
Tam Nguyen 32a757a247 fix: query for ssh keys to show servers, rather than default empty 2025-01-13 16:44:34 +11:00
Mauricio Siu 8e4d8d68ca Merge pull request #1096 from Dokploy/canary
v0.17.0
2025-01-12 19:07:28 -06:00
Mauricio Siu 48e6c40a38 chore: bump version 2025-01-12 18:29:01 -06:00
Mauricio Siu 0a06c0aa28 refactor: add breadcrumb redis 2025-01-12 15:18:36 -06:00
Mauricio Siu a965c0e924 fix: add prefix and remove resolution esm flag (#1095)
* fix: add prefix and remove resolution esm flag

* refactor: revert
2025-01-12 15:10:25 -06:00
Khiet Tam Nguyen ae97595610 feat(template): added Actual Budget, a super fast and privacy-focused app for managing your finances (#1080)
Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
2025-01-12 14:46:11 -06:00
João Gabriel 106a660a2b fix: discord decoration tiny adjustment (#1086) 2025-01-12 14:42:39 -06:00
Alexis Loiseau c25e7c53aa feat(template): added conduit, a matrix homeserver (#1087)
* feat(template): added conduit, a matrix homeserver

* Update apps/dokploy/templates/conduit/index.ts

* Update apps/dokploy/templates/conduit/index.ts

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
2025-01-12 14:41:50 -06:00
Vishal kadam c9308aebc2 style: enhance template selection UI and add view modes toggle (#1094)
* feat: enhance template selection UI and add view modes toggle

* fix: show template tags only in detailed view mode

* refactor: set detailed

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
2025-01-12 14:34:28 -06:00
sao-coding c0a2d2c399 feat(i18n): add missing keys and improve wording in Traditional Chinese translations (#1091)
* feat(i18n): add missing keys to Traditional Chinese translation

* feat(i18n): add missing keys to Traditional Chinese translation
2025-01-12 14:31:07 -06:00
Mauricio Siu a104867ed2 Feat/add sidebar (#1084)
* refactor: add sidebar

* chore: add deps

* refactor: update sidebar

* refactor: another layout

* refactor: update variant

* refactor: change layout

* refactor: change variant

* refactor: enhance sidebar navigation with active state management

* feat: add project button to dashboard

* Merge branch 'canary' into feat/add-sidebar

* refactor: add loader

* refactor: update destinations and refactor

* refactor: ui refactor certificates

* refactor: delete unused files

* refactor: remove unused files and duplicate registry

* refactor: update style registry

* refactor: add new design registry

* refactor: enhance git providers

* refactor: remove duplicate files

* refactor: update

* refactor: update users

* refactor: delete unused files

* refactor: update profile

* refactor: apply changes

* refactor: update UI

* refactor: enhance Docker monitoring UI layout

* refactor: add theme toggle and language selection to user navigation (#1083)

* refactor: remove unused files

* feat: add filter to services

* refactor: add active items

* refactor: remove tab prop

* refactor: remove unused files

* refactor: remove duplicated files

* refactor: remove unused files

* refactor: remove duplicate files

* refactor: remove unused files

* refactor: delete unused files

* refactor: remove unsued files

* refactor: delete unused files

* refactor: lint

* refactor: remove unused secuirty

* refactor: delete unused files

* refactor: delete unused files

* remove imports

* refactor: add update button

* refactor: delete unused files

* refactor: remove unused code

* refactor: remove unused files

* refactor: update login page

* refactor: update login UI

* refactor: update ui reset password

* refactor: add justify end

* feat: add suscriptions

* feat: add sheet

* feat: add logs for postgres

* feat: add logs for all databases

* feat: add server logs with drawer logs

* refactor: remove unused files

* refactor: add refetch when closing

* refactor: fix linter

* chore: bump node-20

* revert

* refactor: fix conflicts

* refactor: update

* refactor: add missing deps

* refactor: delete duplicate files

* refactor: delete unsued files

* chore: lint

* refactor: remove unsued file

* refactor: add refetch

* refactor: remove duplicated files

* refactor: delete unused files

* refactor: update setup onboarding

* refactor: add breadcrumb

* refactor: apply updates

* refactor: add faker

* refactor: use 0 in validation

* refactor: show correct state

* refactor: update

---------

Co-authored-by: vishalkadam47 <vishal@jeevops.com>
Co-authored-by: Vishal kadam <107353260+vishalkadam47@users.noreply.github.com>
2025-01-12 14:29:43 -06:00
Tobias Barsnes 87f4c7b71b refactor: better focus-visible a11y (#1017)
* refactor: better focus-visible a11y

* style: fix tree leaf width

* style: input focus ring size

* refactor: focus a11y on project pages

* fix: project-environment import statement

* style: `ring-border` on input

* refactor: use ring border

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
2025-01-11 17:42:05 -06:00
shiqocred d2094d6d76 Update notification.ts (#6)
fix router notification
2025-01-12 02:14:09 +07:00
shiqocred c0b8a411bd Update build-success.ts (#5)
add format
2025-01-12 02:04:10 +07:00
shiqocred 1d8db07fa1 Add inline button telegram (#4)
* Update utils.ts

add type inline button

* Update dokploy-restart.ts

fixing format massage and adding [] for inline button type

* Update docker-cleanup.ts

fixing telegram message

* Update database-backup.ts

fixing telegram message

* Update build-error.ts

fixing message and adding button logs view

* Update build-success.ts

fixing message, adding domains props, adding inline button

* Update compose.ts

adding get domains compose and send to notif

* Update application.ts

adding get domains and send it to notif

* Update build-success.ts

fix space

* Update dokploy-restart.ts

fixing space
2025-01-12 01:20:39 +07:00
shiqocred dd3618bfd9 Add inline button telegram (#3)
* Update utils.ts

add type inline button

* Update dokploy-restart.ts

fixing format massage and adding [] for inline button type

* Update docker-cleanup.ts

fixing telegram message

* Update database-backup.ts

fixing telegram message

* Update build-error.ts

fixing message and adding button logs view

* Update build-success.ts

fixing message, adding domains props, adding inline button

* Update compose.ts

adding get domains compose and send to notif

* Update application.ts

adding get domains and send it to notif
2025-01-12 00:55:31 +07:00
Khiet Tam Nguyen 9db979e43f fix(ui): full width for body tag (#1078) 2025-01-10 22:02:40 -06:00
shiqocred 553ae70656 feat(i18n): indonesian language (#1082)
* Update languages.ts

* Create common.json

* Create settings.json
2025-01-10 22:01:53 -06:00
Mauricio Siu 15fd663972 Merge pull request #1066 from szwabodev/fix/localServerTerminal
fix: connection to local server terminal
2025-01-10 00:45:11 -06:00
Mauricio Siu 123605dc0d Merge pull request #1068 from yiiman-dev/canary
Update wordpress version to latest
2025-01-08 22:20:38 -06:00
Mauricio Siu d3f3265728 Merge pull request #1065 from nktnet1/gotenberg-template
feat(template): added Gotenberg, a Docker-powered stateless API for PDF files
2025-01-08 22:19:10 -06:00
Mauricio Siu 1a956314f8 Update apps/dokploy/templates/gotenberg/docker-compose.yml 2025-01-08 22:17:07 -06:00
Mauricio Siu 15cc8440f2 Merge pull request #1064 from nktnet1/template-generateBase64-import-index.ts
docs(contribution): fix template index.ts
2025-01-08 22:15:21 -06:00
Tam Nguyen 4a9d7225c9 chore: typo "accesed" changed to "accessed" for TS code 2025-01-08 13:44:59 +11:00
Saman Beheshtian (YiiMan) 103654d678 Update wordpress version to latest 2025-01-08 02:07:19 +00:00
Khiet Tam Nguyen 34a375776e docs: added the "tools" tag for gotenberg 2025-01-07 09:23:58 +11:00
Khiet Tam Nguyen 644fb1ef43 fix: switch to using gotenberg "latest" tag 2025-01-07 09:07:38 +11:00
Khiet Tam Nguyen 4e581bae99 fix: updated gotenberg from 8.15.2 -> 8.15.3 2025-01-07 09:03:49 +11:00
UndefinedPony ee9f4796c3 style: remove spaces from commands 2025-01-06 12:12:23 +01:00
UndefinedPony 41a970c526 feat: add permission grant command when key generation fails 2025-01-06 12:09:54 +01:00
UndefinedPony 76c6d02566 feat: add ssh key comment for auto generated key 2025-01-06 12:09:15 +01:00
UndefinedPony d0a5427c66 fix: connecting to local server terminal 2025-01-06 11:41:34 +01:00
UndefinedPony aa3541b67b feat: add utils to get docker host and check if running on WSL 2025-01-06 11:39:19 +01:00
UndefinedPony c31ed2b2b0 feat: add iproute2 as dependency to dockerfile 2025-01-06 11:38:08 +01:00
Khiet Tam Nguyen a42b0ba32b docs(template): more concise description for gotenberg 2025-01-06 19:20:40 +11:00
Tam Nguyen 6866da97dd feat(template): added gotenberg, a pdf API service 2025-01-06 15:26:18 +11:00
Tam Nguyen fdaea01b5b docs(contribution): added missing generateBase64 import for template index.ts 2025-01-06 14:00:43 +11:00
Khiet Tam Nguyen fa2d81d2e7 docs(contribution): added missing generateBase64 import for template index.ts 2025-01-06 13:50:38 +11:00
Mauricio Siu 332416b7e7 Merge pull request #1054 from kerimovok/canary
feat(i18n): azerbaijani language
2025-01-05 15:34:17 -06:00
Mauricio Siu 7a4ee76eb6 Merge pull request #1039 from CyberHotline/canary
Added GLPI template
2025-01-05 14:50:09 -06:00
Mauricio Siu 0c304bd304 Merge pull request #1053 from SIPC/patch-1
Updated Chinese translation
2025-01-05 14:48:34 -06:00
Orkhan Karimov 8314c1a0a5 fix(indention): tab -> 4 spaces 2025-01-06 00:16:09 +04:00
Mauricio Siu 46a4e31e71 chore: add new sponsors 2025-01-03 16:19:48 -06:00
Mauricio Siu acd3e0dd7d Merge pull request #1060 from jmsx/canary
fix: Update Vaultwarden version to 1.32.7 [CVE fix]
2025-01-03 14:58:18 -06:00
Mohab Gabber f4e7a4d79a Merge branch 'canary' into canary 2025-01-03 19:11:33 +02:00
Jose Manuel Gonzalez c7e5eba086 fix: Update Vaultwarden version to 1.32.7 [CVE fix] 2025-01-03 10:51:42 +01:00
Mauricio Siu 65361d18c2 Merge pull request #1055 from wish-oss/fix/console-error
refactor: remove unnecessary error logging in GPU setup functions
2025-01-02 16:21:04 -06:00
Mohab Gabber bb5fd9895d fix: update MySQL image reference in docker-compose.yml 2025-01-02 21:28:52 +02:00
vishalkadam47 49a6b72c60 refactor: remove unnecessary error logging in GPU setup functions 2025-01-02 21:01:03 +05:30
Orkhan Karimov 84a88299ea feat(i18n): azerbaijani language 2025-01-02 16:27:57 +04:00
ink d0fe635620 Update settings.json 2025-01-02 19:43:20 +08:00
Mauricio Siu 41d4ff8489 Merge pull request #1051 from wyattjoh/canary
fix: added unzip to server setup
2025-01-02 01:25:32 -06:00
Mauricio Siu b9d8a48ae6 Merge pull request #1052 from Dokploy/1050-dokploy-ignore-run-command
refactor: add text for run command
2025-01-01 19:29:48 -06:00
Mauricio Siu 3e8708d2b9 refactor: add text for run command 2025-01-01 19:29:33 -06:00
Mauricio Siu 2b0232a24c Merge pull request #1047 from Pi-Bouf/environment-style-improvement
style: environment style improvement
2025-01-01 19:25:26 -06:00
Mauricio Siu cbdea7cf48 Merge pull request #1049 from designorant/style/favicon-theme
style: enhance favicon with dark mode support
2025-01-01 19:19:56 -06:00
Mauricio Siu f042cb720f Merge pull request #1042 from szwabodev/feat/newUpdateModalInNavbar
feat: use check updates modal for update available in navbar
2025-01-01 19:15:12 -06:00
Mauricio Siu 02b977bfc4 Merge pull request #1037 from 190km/feat-shows-req-when-is-active
feat: shows req when is active
2025-01-01 19:05:47 -06:00
Mauricio Siu 9f24f24de3 refactor: improve requests 2025-01-01 19:05:06 -06:00
Mauricio Siu cbc8c24985 Merge pull request #1038 from 190km/swarm-style-improvement
style: swarm style improvement
2025-01-01 19:02:57 -06:00
Mauricio Siu b17369264c refactor: remove min-h-screen 2025-01-01 19:02:24 -06:00
Mauricio Siu 9c783177c8 Merge pull request #1033 from DJKnaeckebrot/feature/check-mate-template
feat(template): add checkmate template
2025-01-01 18:57:32 -06:00
Mauricio Siu f2159a3439 Update apps/dokploy/templates/checkmate/docker-compose.yml 2025-01-01 18:57:27 -06:00
Mauricio Siu eec0a55212 Update apps/dokploy/templates/checkmate/docker-compose.yml 2025-01-01 18:57:23 -06:00
Mauricio Siu 16bab629de Merge pull request #1030 from DJKnaeckebrot/feature/fix-2fa-style
style(2fa): make pin input centered and make boarder more white to make it more visible
2025-01-01 18:54:49 -06:00
Mauricio Siu 592bf9292e Merge pull request #1032 from DJKnaeckebrot/feature/fix-huly-template
fix(huly-template): resolve issue with wrong domain
2025-01-01 18:54:33 -06:00
Mauricio Siu b93d26f937 refactor: use shadcn ui classes 2025-01-01 18:53:30 -06:00
Mauricio Siu c0e7f4ad8c Merge pull request #1029 from wish-oss/refactor/port-mapping-ui
refactor: enhance ManageTraefikPorts with ScrollArea for better UI
2025-01-01 18:52:09 -06:00
Mauricio Siu 05cf51bfb3 Merge pull request #1028 from designorant/feat/complete-polish
feat(i18n): add missing keys to Polish translation
2025-01-01 18:48:10 -06:00
Wyatt Johnson 7cf5cb4032 fix: added unzip to server setup
The installation script for rclone requires unzip to be present
in order to install. This adds this dep to the deps installed
as it's not present on Debian.
2025-01-01 16:44:41 -07:00
Michał Ordon 76787b9056 style: enhance favicon with dark mode support 2025-01-01 17:46:39 +00:00
Pierre B 513b17105e style: environment style improvement 2025-01-01 15:47:47 +01:00
UndefinedPony 187f051484 fix: check health endpoint instead of awaiting service restart to fix update success status 2024-12-31 11:20:59 +01:00
UndefinedPony 2be79304fb refactor: allow using already fetched updateData, remove ping icon 2024-12-31 11:19:22 +01:00
UndefinedPony 29a8cb63c0 refactor: pass update data and use updates modal in navbar 2024-12-31 11:13:58 +01:00
UndefinedPony ed5936ede7 feat: add health endpoint for frontend app 2024-12-31 11:11:33 +01:00
Mohab Gabber d4f89425db Added GLPI template 2024-12-30 23:45:59 +02:00
190km a7983f32a6 style: added gap for cards title & gap for tls status & availability 2024-12-30 20:05:44 +01:00
190km 198ace236b feat: shows req when is active 2024-12-30 19:23:31 +01:00
djknaeckebrot 931f3fc28e feat: add checkmate template 2024-12-30 16:08:04 +01:00
DJKnaeckebrot a4cc3b619a fix: resolve issue with wrong domain 2024-12-30 11:07:28 +01:00
DJKnaeckebrot cb5077cfcc style(2fa): make pin input centered and make boarder more white to make it more visible 2024-12-30 10:54:47 +01:00
vishalkadam47 9122a1e4b2 refactor: enhance ManageTraefikPorts with ScrollArea for better UI and port management display 2024-12-30 07:32:34 +05:30
Michał Ordon b2cf442d9b feat(i18n): add missing keys to Polish translation 2024-12-30 01:52:09 +00:00
Mauricio Siu 79b733536f Merge branch 'canary' into feat/stack-env-support 2024-12-08 18:35:40 -06:00
xenonwellz 65ee0a3e22 fix(builder): created processed file in the same directory as main stack.yml 2024-11-11 02:22:48 +01:00
xenonwellz c9b570e469 fix(builder): fixed issues on non-raw compose and external servers 2024-11-09 13:00:15 +01:00
xenonwellz dafed3096f refactor(builder): removed path log 2024-11-04 22:47:58 +01:00
xenonwellz f772fec407 fix(bundler): docker-compose bug 2024-11-04 11:02:46 +01:00
xenonwellz 06cbd1fce1 refactor(bundler): removed redundant code 2024-11-03 15:26:54 +01:00
xenonwellz 9c355bcfb7 refactor(builder): removed unused and redundant code 2024-11-02 12:42:53 +01:00
xenonwellz 06081627e8 refactor: used docker stack config 2024-11-02 00:35:38 +01:00
xenonwellz dc1e12d6ed feat(compose): added stop functionality for stack 2024-11-01 23:35:42 +01:00
xenonwellz cb02deb837 fix(builder): fixed docker-compose issue 2024-11-01 23:15:12 +01:00
xenonwellz 94786c738b feat: added env support for dokploy 2024-11-01 15:27:00 +01:00
556 changed files with 80652 additions and 28144 deletions
-119
View File
@@ -1,119 +0,0 @@
version: 2.1
jobs:
build-amd64:
machine:
image: ubuntu-2004:current
steps:
- checkout
- run:
name: Prepare .env file
command: |
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- run:
name: Build and push AMD64 image
command: |
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "main" ]; then
TAG="latest"
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
else
TAG="feature"
fi
docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 .
docker push dokploy/dokploy:${TAG}-amd64
build-arm64:
machine:
image: ubuntu-2004:current
resource_class: arm.large
steps:
- checkout
- run:
name: Prepare .env file
command: |
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- run:
name: Build and push ARM64 image
command: |
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "main" ]; then
TAG="latest"
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
else
TAG="feature"
fi
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
docker push dokploy/dokploy:${TAG}-arm64
combine-manifests:
docker:
- image: cimg/node:18.18.0
steps:
- checkout
- setup_remote_docker
- run:
name: Create and push multi-arch manifest
command: |
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "main" ]; then
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
echo $VERSION
TAG="latest"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
docker manifest create dokploy/dokploy:${VERSION} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${VERSION}
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
else
TAG="feature"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
fi
workflows:
build-all:
jobs:
- build-amd64:
filters:
branches:
only:
- main
- canary
- fix/nixpacks-version
- build-arm64:
filters:
branches:
only:
- main
- canary
- fix/nixpacks-version
- combine-manifests:
requires:
- build-amd64
- build-arm64
filters:
branches:
only:
- main
- canary
- fix/nixpacks-version
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+83
View File
@@ -0,0 +1,83 @@
name: Auto PR to main when version changes
on:
push:
branches:
- canary
permissions:
contents: write
pull-requests: write
jobs:
create-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version from package.json
id: package_version
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
- name: Get latest GitHub tag
id: latest_tag
run: |
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1)
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
echo $LATEST_TAG
- name: Compare versions
id: compare_versions
run: |
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
VERSION_CHANGED="true"
else
VERSION_CHANGED="false"
fi
echo "VERSION_CHANGED=$VERSION_CHANGED" >> $GITHUB_ENV
echo "Comparing versions:"
echo "Current version: ${{ env.VERSION }}"
echo "Latest tag: ${{ env.LATEST_TAG }}"
echo "Version changed: $VERSION_CHANGED"
- name: Check if a PR already exists
id: check_pr
run: |
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
- name: Create Pull Request
if: env.VERSION_CHANGED == 'true' && env.PR_EXISTS == '0'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin main
git checkout canary
git push origin canary
gh pr create \
--title "🚀 Release ${{ env.VERSION }}" \
--body '
This PR promotes changes from `canary` to `main` for version ${{ env.VERSION }}.
### 🔍 Changes Include:
- Version bump to ${{ env.VERSION }}
- All changes from canary branch
### ✅ Pre-merge Checklist:
- [ ] All tests passing
- [ ] Documentation updated
- [ ] Docker images built and tested
> 🤖 This PR was automatically generated by [GitHub Actions](https://github.com/actions)' \
--base main \
--head canary \
--label "release" --label "automated pr" || true \
--reviewer siumauricio \
--assignee siumauricio
env:
GH_TOKEN: ${{ github.token }}
+4 -5
View File
@@ -2,7 +2,7 @@ name: Build Docker images
on:
push:
branches: ["canary", "main"]
branches: ["canary", "main", "feat/monitoring"]
jobs:
build-and-push-cloud-image:
@@ -17,7 +17,7 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
@@ -53,8 +53,7 @@ jobs:
push: true
tags: |
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
platforms: linux/amd64
platforms: linux/amd64
build-and-push-server-image:
runs-on: ubuntu-latest
@@ -77,4 +76,4 @@ jobs:
push: true
tags: |
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
platforms: linux/amd64
platforms: linux/amd64
+161
View File
@@ -0,0 +1,161 @@
name: Dokploy Docker Build
on:
push:
branches: [main, canary, "feat/monitoring"]
env:
IMAGE_NAME: dokploy/dokploy
jobs:
docker-amd:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set tag and version
id: meta
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAG="latest"
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
else
TAG="feature"
fi
echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT
- name: Prepare env file
run: |
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
docker-arm:
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set tag and version
id: meta
run: |
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAG="latest"
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
else
TAG="feature"
fi
echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT
- name: Prepare env file
run: |
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
combine-manifests:
needs: [docker-amd, docker-arm]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push manifests
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
TAG="latest"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
docker buildx imagetools create -t ${IMAGE_NAME}:${VERSION} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
else
TAG="feature"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
fi
generate-release:
needs: [combine-manifests]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version
id: get_version
run: |
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.get_version.outputs.version }}
name: ${{ steps.get_version.outputs.version }}
generate_release_notes: true
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+118
View File
@@ -0,0 +1,118 @@
name: Dokploy Monitoring Build
on:
push:
branches: [main, canary]
env:
IMAGE_NAME: dokploy/monitoring
jobs:
docker-amd:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set tag
id: meta
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAG="latest"
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
else
TAG="feature"
fi
echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.monitoring
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
docker-arm:
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set
id: meta
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAG="latest"
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
else
TAG="feature"
fi
echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.monitoring
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
combine-manifests:
needs: [docker-amd, docker-arm]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push manifests
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAG="latest"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
else
TAG="feature"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
fi
+3 -3
View File
@@ -12,7 +12,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 18.18.0
node-version: 20.9.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
@@ -26,7 +26,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 18.18.0
node-version: 20.9.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
@@ -39,7 +39,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 18.18.0
node-version: 20.9.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
+3
View File
@@ -39,3 +39,6 @@ yarn-error.log*
# Misc
.DS_Store
*.pem
.db
+1 -1
View File
@@ -1 +1 @@
18.18.0
20.9.0
+8 -7
View File
@@ -14,12 +14,10 @@ We have a few guidelines to follow when contributing to this project:
## Commit Convention
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
### Commit Message Format
```
<type>[optional scope]: <description>
@@ -54,6 +52,8 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.9.0
```bash
git clone https://github.com/dokploy/dokploy.git
cd dokploy
@@ -73,9 +73,10 @@ Run the command that will spin up all the required services and files.
pnpm run dokploy:setup
```
Run this script
Run this script
```bash
pnpm run server:script
pnpm run server:script
```
Now run the development server.
@@ -169,6 +170,7 @@ Let's take the example of `plausible` template.
```typescript
// EXAMPLE
import {
generateBase64,
generateHash,
generateRandomDomain,
type Template,
@@ -200,8 +202,8 @@ export function generate(schema: Schema): Template {
const mounts: Template["mounts"] = [
{
mountPath: "./clickhouse/clickhouse-config.xml",
content: `some content......`,
filePath: "./clickhouse/clickhouse-config.xml",
content: "some content......",
},
];
@@ -247,4 +249,3 @@ export function generate(schema: Schema): Template {
## Docs & Website
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
+3 -3
View File
@@ -1,4 +1,4 @@
FROM node:18-slim AS base
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
@@ -7,7 +7,7 @@ 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/*
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
@@ -29,7 +29,7 @@ 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/*
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:18-slim AS base
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
@@ -7,7 +7,7 @@ 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/*
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && 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
+41
View File
@@ -0,0 +1,41 @@
# Build stage
FROM golang:1.21-alpine3.19 AS builder
# Instalar dependencias necesarias
RUN apk add --no-cache gcc musl-dev sqlite-dev
# Establecer el directorio de trabajo
WORKDIR /app
# Copiar todo el código fuente primero
COPY . .
# Movernos al directorio de la aplicación golang
WORKDIR /app/apps/monitoring
# Descargar dependencias
RUN go mod download
# Compilar la aplicación
RUN CGO_ENABLED=1 GOOS=linux go build -o main main.go
# Etapa final
FROM alpine:3.19
# Instalar SQLite y otras dependencias necesarias
RUN apk add --no-cache sqlite-libs docker-cli
WORKDIR /app
# Copiar el binario compilado y el archivo monitor.go
COPY --from=builder /app/apps/monitoring/main ./main
COPY --from=builder /app/apps/monitoring/main.go ./monitor.go
# COPY --from=builder /app/apps/golang/.env ./.env
# Exponer el puerto
ENV PORT=3001
EXPOSE 3001
# Ejecutar la aplicación
CMD ["./main"]
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:18-slim AS base
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
@@ -7,7 +7,7 @@ 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/*
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && 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
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:18-slim AS base
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
@@ -7,7 +7,7 @@ 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/*
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && 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
+4
View File
@@ -71,6 +71,9 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<a href="https://mandarin3d.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/mandarin.png" alt="Mandarin" height="50"/>
</a>
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
</a>
</div>
### Premium Supporters 🥇
@@ -89,6 +92,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
</div>
### Community Backers 🤝
+1 -1
View File
@@ -5,7 +5,7 @@
"scripts": {
"dev": "PORT=4000 tsx watch src/index.ts",
"build": "tsc --project tsconfig.json",
"start": "node --experimental-specifier-resolution=node dist/index.js",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
"dependencies": {
+3 -3
View File
@@ -4,9 +4,9 @@ import "dotenv/config";
import { zValidator } from "@hono/zod-validator";
import { Queue } from "@nerimity/mimiqueue";
import { createClient } from "redis";
import { logger } from "./logger";
import { type DeployJob, deployJobSchema } from "./schema";
import { deploy } from "./utils";
import { logger } from "./logger.js";
import { type DeployJob, deployJobSchema } from "./schema.js";
import { deploy } from "./utils.js";
const app = new Hono();
const redisClient = createClient({
+1 -1
View File
@@ -1 +1 @@
18.18.0
20.9.0
@@ -0,0 +1,98 @@
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
import { describe, expect, it } from "vitest";
describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = {
"x-github-event": "push",
};
const createMockBody = (message: string) => ({
head_commit: {
message,
},
});
const skipKeywords = [
"[skip ci]",
"[ci skip]",
"[no ci]",
"[skip actions]",
"[actions skip]",
];
it("should detect skip keywords in commit message", () => {
for (const keyword of skipKeywords) {
const message = `feat: add new feature ${keyword}`;
const commitMessage = extractCommitMessage(
mockGithubHeaders,
createMockBody(message),
);
expect(commitMessage.includes(keyword)).toBe(true);
}
});
it("should not detect skip keywords in normal commit message", () => {
const message = "feat: add new feature";
const commitMessage = extractCommitMessage(
mockGithubHeaders,
createMockBody(message),
);
for (const keyword of skipKeywords) {
expect(commitMessage.includes(keyword)).toBe(false);
}
});
it("should handle different webhook sources", () => {
// GitHub
expect(
extractCommitMessage(
{ "x-github-event": "push" },
{ head_commit: { message: "[skip ci] test" } },
),
).toBe("[skip ci] test");
// GitLab
expect(
extractCommitMessage(
{ "x-gitlab-event": "push" },
{ commits: [{ message: "[skip ci] test" }] },
),
).toBe("[skip ci] test");
// Bitbucket
expect(
extractCommitMessage(
{ "x-event-key": "repo:push" },
{
push: {
changes: [{ new: { target: { message: "[skip ci] test" } } }],
},
},
),
).toBe("[skip ci] test");
// Gitea
expect(
extractCommitMessage(
{ "x-gitea-event": "push" },
{ commits: [{ message: "[skip ci] test" }] },
),
).toBe("[skip ci] test");
});
it("should handle missing commit message", () => {
expect(extractCommitMessage(mockGithubHeaders, {})).toBe("NEW COMMIT");
expect(extractCommitMessage({ "x-gitlab-event": "push" }, {})).toBe(
"NEW COMMIT",
);
expect(
extractCommitMessage(
{ "x-event-key": "repo:push" },
{ push: { changes: [] } },
),
).toBe("NEW COMMIT");
expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe(
"NEW COMMIT",
);
});
});
@@ -14,6 +14,32 @@ import {
import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: Admin = {
enablePaidFeatures: false,
metricsConfig: {
containers: {
refreshRate: 20,
services: {
include: [],
exclude: [],
},
},
server: {
type: "Dokploy",
cronJob: "",
port: 4500,
refreshRate: 20,
retentionDays: 2,
token: "",
thresholds: {
cpu: 0,
memory: 0,
},
urlCallback: "",
},
},
cleanupCacheApplications: false,
cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false,
createdAt: "",
authId: "",
adminId: "string",
+1
View File
@@ -13,6 +13,7 @@ export default defineConfig({
NODE: "test",
},
},
plugins: [tsconfigPaths()],
resolve: {
alias: {
"@dokploy/server": path.resolve(
+19 -11
View File
@@ -13,10 +13,12 @@ import { CardTitle } from "@/components/ui/card";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { AlertTriangle } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect } from "react";
@@ -93,19 +95,25 @@ export const Login2FA = ({ authId }: Props) => {
control={form.control}
name="pin"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormItem className="flex flex-col max-sm:items-center">
<FormLabel>Pin</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<div className="flex">
<InputOTP
maxLength={6}
{...field}
pattern={REGEXP_ONLY_DIGITS}
>
<InputOTPGroup>
<InputOTPSlot index={0} className="border-border" />
<InputOTPSlot index={1} className="border-border" />
<InputOTPSlot index={2} className="border-border" />
<InputOTPSlot index={3} className="border-border" />
<InputOTPSlot index={4} className="border-border" />
<InputOTPSlot index={5} className="border-border" />
</InputOTPGroup>
</InputOTP>
</div>
</FormControl>
<FormDescription>
Please enter the 6 digits code provided by your authenticator
@@ -81,7 +81,8 @@ export const AddCommand = ({ applicationId }: Props) => {
<div>
<CardTitle className="text-xl">Run Command</CardTitle>
<CardDescription>
Run a custom command in the container
Run a custom command in the container after the application
initialized
</CardDescription>
</div>
</CardHeader>
@@ -1,63 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { TrashIcon } from "lucide-react";
import { toast } from "sonner";
interface Props {
portId: string;
}
export const DeletePort = ({ portId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isLoading } = api.port.delete.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the port
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
portId,
})
.then((data) => {
utils.application.one.invalidate({
applicationId: data?.applicationId,
});
toast.success("Port delete successfully");
})
.catch(() => {
toast.error("Error deleting the port");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -27,7 +27,7 @@ import {
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -45,18 +45,29 @@ type AddPort = z.infer<typeof AddPortSchema>;
interface Props {
applicationId: string;
portId?: string;
children?: React.ReactNode;
}
export const AddPort = ({
export const HandlePorts = ({
applicationId,
portId,
children = <PlusIcon className="h-4 w-4" />,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } =
api.port.create.useMutation();
const { data } = api.port.one.useQuery(
{
portId: portId ?? "",
},
{
enabled: !!portId,
},
);
const { mutateAsync, isLoading, error, isError } = portId
? api.port.update.useMutation()
: api.port.create.useMutation();
const form = useForm<AddPort>({
defaultValues: {
@@ -68,32 +79,46 @@ export const AddPort = ({
useEffect(() => {
form.reset({
publishedPort: 0,
targetPort: 0,
publishedPort: data?.publishedPort ?? 0,
targetPort: data?.targetPort ?? 0,
protocol: data?.protocol ?? "tcp",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (data: AddPort) => {
await mutateAsync({
applicationId,
...data,
portId: portId || "",
})
.then(async () => {
toast.success("Port Created");
toast.success(portId ? "Port Updated" : "Port Created");
await utils.application.one.invalidate({
applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error creating the port");
toast.error(
portId ? "Error updating the port" : "Error creating the port",
);
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>{children}</Button>
{portId ? (
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
) : (
<Button>{children}</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
@@ -204,7 +229,7 @@ export const AddPort = ({
form="hook-form-add-port"
type="submit"
>
Create
{portId ? "Update" : "Create"}
</Button>
</DialogFooter>
</Form>
@@ -1,4 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -7,23 +9,25 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Rss } from "lucide-react";
import { Rss, Trash2 } from "lucide-react";
import React from "react";
import { AddPort } from "./add-port";
import { DeletePort } from "./delete-port";
import { UpdatePort } from "./update-port";
import { toast } from "sonner";
import { HandlePorts } from "./handle-ports";
interface Props {
applicationId: string;
}
export const ShowPorts = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery(
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { mutateAsync: deletePort, isLoading: isRemoving } =
api.port.delete.useMutation();
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
@@ -35,7 +39,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
</div>
{data && data?.ports.length > 0 && (
<AddPort applicationId={applicationId}>Add Port</AddPort>
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
@@ -45,7 +49,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
<span className="text-base text-muted-foreground">
No ports configured
</span>
<AddPort applicationId={applicationId}>Add Port</AddPort>
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
</div>
) : (
<div className="flex flex-col pt-2 gap-4">
@@ -78,8 +82,36 @@ export const ShowPorts = ({ applicationId }: Props) => {
</div>
</div>
<div className="flex flex-row gap-4">
<UpdatePort portId={port.portId} />
<DeletePort portId={port.portId} />
<HandlePorts
applicationId={applicationId}
portId={port.portId}
/>
<DialogAction
title="Delete Port"
description="Are you sure you want to delete this port?"
type="destructive"
onClick={async () => {
await deletePort({
portId: port.portId,
})
.then(() => {
refetch();
toast.success("Port deleted successfully");
})
.catch(() => {
toast.error("Error deleting port");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
@@ -1,195 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input, NumberInput } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const UpdatePortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535),
targetPort: z.number().int().min(1).max(65535),
protocol: z.enum(["tcp", "udp"], {
required_error: "Protocol is required",
invalid_type_error: "Protocol must be a valid protocol",
}),
});
type UpdatePort = z.infer<typeof UpdatePortSchema>;
interface Props {
portId: string;
}
export const UpdatePort = ({ portId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { data } = api.port.one.useQuery(
{
portId,
},
{
enabled: !!portId,
},
);
const { mutateAsync, isLoading, error, isError } =
api.port.update.useMutation();
const form = useForm<UpdatePort>({
defaultValues: {},
resolver: zodResolver(UpdatePortSchema),
});
useEffect(() => {
if (data) {
form.reset({
publishedPort: data.publishedPort,
targetPort: data.targetPort,
protocol: data.protocol,
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdatePort) => {
await mutateAsync({
portId,
publishedPort: data.publishedPort,
targetPort: data.targetPort,
protocol: data.protocol,
})
.then(async (response) => {
toast.success("Port Updated");
await utils.application.one.invalidate({
applicationId: response?.applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error updating the port");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Update</DialogTitle>
<DialogDescription>Update the port</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-redirect"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="publishedPort"
render={({ field }) => (
<FormItem>
<FormLabel>Published Port</FormLabel>
<FormControl>
<NumberInput placeholder="1-65535" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetPort"
render={({ field }) => (
<FormItem>
<FormLabel>Target Port</FormLabel>
<FormControl>
<NumberInput placeholder="1-65535" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="protocol"
render={({ field }) => {
return (
<FormItem className="md:col-span-2">
<FormLabel>Protocol</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a protocol" />
</SelectTrigger>
</FormControl>
<SelectContent defaultValue={"none"}>
<SelectItem value={"none"} disabled>
None
</SelectItem>
<SelectItem value={"tcp"}>TCP</SelectItem>
<SelectItem value={"udp"}>UDP</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-update-redirect"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -1,66 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { TrashIcon } from "lucide-react";
import { toast } from "sonner";
interface Props {
redirectId: string;
}
export const DeleteRedirect = ({ redirectId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isLoading } = api.redirects.delete.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
redirect
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
redirectId,
})
.then((data) => {
utils.application.one.invalidate({
applicationId: data?.applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: data?.applicationId,
});
toast.success("Redirect delete successfully");
})
.catch(() => {
toast.error("Error deleting the redirect");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -31,7 +31,7 @@ import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -77,19 +77,32 @@ const redirectPresets = [
interface Props {
applicationId: string;
redirectId?: string;
children?: React.ReactNode;
}
export const AddRedirect = ({
export const HandleRedirect = ({
applicationId,
redirectId,
children = <PlusIcon className="w-4 h-4" />,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [presetSelected, setPresetSelected] = useState("");
const { data, refetch } = api.redirects.one.useQuery(
{
redirectId: redirectId || "",
},
{
enabled: !!redirectId,
},
);
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } =
api.redirects.create.useMutation();
const { mutateAsync, isLoading, error, isError } = redirectId
? api.redirects.update.useMutation()
: api.redirects.create.useMutation();
const form = useForm<AddRedirect>({
defaultValues: {
@@ -102,29 +115,35 @@ export const AddRedirect = ({
useEffect(() => {
form.reset({
permanent: false,
regex: "",
replacement: "",
permanent: data?.permanent || false,
regex: data?.regex || "",
replacement: data?.replacement || "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (data: AddRedirect) => {
await mutateAsync({
applicationId,
...data,
redirectId: redirectId || "",
})
.then(async () => {
toast.success("Redirect Created");
toast.success(redirectId ? "Redirect Updated" : "Redirect Created");
await utils.application.one.invalidate({
applicationId,
});
refetch();
await utils.application.readTraefikConfig.invalidate({
applicationId,
});
onDialogToggle(false);
})
.catch(() => {
toast.error("Error creating the redirect");
toast.error(
redirectId
? "Error updating the redirect"
: "Error creating the redirect",
);
});
};
@@ -148,7 +167,17 @@ export const AddRedirect = ({
return (
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
<DialogTrigger asChild>
<Button>{children}</Button>
{redirectId ? (
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
) : (
<Button>{children}</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
@@ -243,7 +272,7 @@ export const AddRedirect = ({
form="hook-form-add-redirect"
type="submit"
>
Create
{redirectId ? "Update" : "Create"}
</Button>
</DialogFooter>
</Form>
@@ -1,3 +1,5 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -6,23 +8,28 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Split } from "lucide-react";
import { Split, Trash2 } from "lucide-react";
import React from "react";
import { AddRedirect } from "./add-redirect";
import { DeleteRedirect } from "./delete-redirect";
import { UpdateRedirect } from "./update-redirect";
import { toast } from "sonner";
import { HandleRedirect } from "./handle-redirect";
interface Props {
applicationId: string;
}
export const ShowRedirects = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery(
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
api.redirects.delete.useMutation();
const utils = api.useUtils();
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
@@ -35,7 +42,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
</div>
{data && data?.redirects.length > 0 && (
<AddRedirect applicationId={applicationId}>Add Redirect</AddRedirect>
<HandleRedirect applicationId={applicationId}>
Add Redirect
</HandleRedirect>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
@@ -45,9 +54,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
<span className="text-base text-muted-foreground">
No redirects configured
</span>
<AddRedirect applicationId={applicationId}>
<HandleRedirect applicationId={applicationId}>
Add Redirect
</AddRedirect>
</HandleRedirect>
</div>
) : (
<div className="flex flex-col pt-2">
@@ -76,8 +85,40 @@ export const ShowRedirects = ({ applicationId }: Props) => {
</div>
</div>
<div className="flex flex-row gap-4">
<UpdateRedirect redirectId={redirect.redirectId} />
<DeleteRedirect redirectId={redirect.redirectId} />
<HandleRedirect
redirectId={redirect.redirectId}
applicationId={applicationId}
/>
<DialogAction
title="Delete Redirect"
description="Are you sure you want to delete this redirect?"
type="destructive"
onClick={async () => {
await deleteRedirect({
redirectId: redirect.redirectId,
})
.then(() => {
refetch();
utils.application.readTraefikConfig.invalidate({
applicationId,
});
toast.success("Redirect deleted successfully");
})
.catch(() => {
toast.error("Error deleting redirect");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
@@ -1,182 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const UpdateRedirectSchema = z.object({
regex: z.string().min(1, "Regex required"),
permanent: z.boolean().default(false),
replacement: z.string().min(1, "Replacement required"),
});
type UpdateRedirect = z.infer<typeof UpdateRedirectSchema>;
interface Props {
redirectId: string;
}
export const UpdateRedirect = ({ redirectId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data } = api.redirects.one.useQuery(
{
redirectId,
},
{
enabled: !!redirectId,
},
);
const { mutateAsync, isLoading, error, isError } =
api.redirects.update.useMutation();
const form = useForm<UpdateRedirect>({
defaultValues: {
permanent: false,
regex: "",
replacement: "",
},
resolver: zodResolver(UpdateRedirectSchema),
});
useEffect(() => {
if (data) {
form.reset({
permanent: data.permanent || false,
regex: data.regex || "",
replacement: data.replacement || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateRedirect) => {
await mutateAsync({
redirectId,
permanent: data.permanent,
regex: data.regex,
replacement: data.replacement,
})
.then(async (response) => {
toast.success("Redirect Updated");
await utils.application.one.invalidate({
applicationId: response?.applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error updating the redirect");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Update</DialogTitle>
<DialogDescription>Update the redirect</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-redirect"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="regex"
render={({ field }) => (
<FormItem>
<FormLabel>Regex</FormLabel>
<FormControl>
<Input placeholder="^http://localhost/(.*)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="replacement"
render={({ field }) => (
<FormItem>
<FormLabel>Replacement</FormLabel>
<FormControl>
<Input placeholder="http://mydomain/$${1}" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="permanent"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Permanent</FormLabel>
<FormDescription>
Set the permanent option to true to apply a permanent
redirection.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-update-redirect"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -1,66 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { TrashIcon } from "lucide-react";
import { toast } from "sonner";
interface Props {
securityId: string;
}
export const DeleteSecurity = ({ securityId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isLoading } = api.security.delete.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
security
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
securityId,
})
.then((data) => {
utils.application.one.invalidate({
applicationId: data?.applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: data?.applicationId,
});
toast.success("Security delete successfully");
})
.catch(() => {
toast.error("Error deleting the security");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -20,7 +20,7 @@ import {
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -35,17 +35,29 @@ type AddSecurity = z.infer<typeof AddSecuritychema>;
interface Props {
applicationId: string;
securityId?: string;
children?: React.ReactNode;
}
export const AddSecurity = ({
export const HandleSecurity = ({
applicationId,
securityId,
children = <PlusIcon className="h-4 w-4" />,
}: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading, error, isError } =
api.security.create.useMutation();
const { data } = api.security.one.useQuery(
{
securityId: securityId ?? "",
},
{
enabled: !!securityId,
},
);
const { mutateAsync, isLoading, error, isError } = securityId
? api.security.update.useMutation()
: api.security.create.useMutation();
const form = useForm<AddSecurity>({
defaultValues: {
@@ -56,16 +68,20 @@ export const AddSecurity = ({
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
form.reset({
username: data?.username || "",
password: data?.password || "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (data: AddSecurity) => {
await mutateAsync({
applicationId,
...data,
securityId: securityId || "",
})
.then(async () => {
toast.success("Security Created");
toast.success(securityId ? "Security Updated" : "Security Created");
await utils.application.one.invalidate({
applicationId,
});
@@ -75,20 +91,34 @@ export const AddSecurity = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error creating security");
toast.error(
securityId
? "Error updating the security"
: "Error creating security",
);
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>{children}</Button>
{securityId ? (
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
) : (
<Button>{children}</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Security</DialogTitle>
<DialogDescription>
Add security to your application
{securityId ? "Update" : "Add"} security to your application
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
@@ -137,7 +167,7 @@ export const AddSecurity = ({
form="hook-form-add-security"
type="submit"
>
Create
{securityId ? "Update" : "Create"}
</Button>
</DialogFooter>
</Form>
@@ -1,3 +1,5 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -6,23 +8,27 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { LockKeyhole } from "lucide-react";
import { LockKeyhole, Trash2 } from "lucide-react";
import React from "react";
import { AddSecurity } from "./add-security";
import { DeleteSecurity } from "./delete-security";
import { UpdateSecurity } from "./update-security";
import { toast } from "sonner";
import { HandleSecurity } from "./handle-security";
interface Props {
applicationId: string;
}
export const ShowSecurity = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery(
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
api.security.delete.useMutation();
const utils = api.useUtils();
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
@@ -32,7 +38,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
</div>
{data && data?.security.length > 0 && (
<AddSecurity applicationId={applicationId}>Add Security</AddSecurity>
<HandleSecurity applicationId={applicationId}>
Add Security
</HandleSecurity>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
@@ -42,9 +50,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
<span className="text-base text-muted-foreground">
No security configured
</span>
<AddSecurity applicationId={applicationId}>
<HandleSecurity applicationId={applicationId}>
Add Security
</AddSecurity>
</HandleSecurity>
</div>
) : (
<div className="flex flex-col pt-2">
@@ -67,8 +75,39 @@ export const ShowSecurity = ({ applicationId }: Props) => {
</div>
</div>
<div className="flex flex-row gap-2">
<UpdateSecurity securityId={security.securityId} />
<DeleteSecurity securityId={security.securityId} />
<HandleSecurity
securityId={security.securityId}
applicationId={applicationId}
/>
<DialogAction
title="Delete Security"
description="Are you sure you want to delete this security?"
type="destructive"
onClick={async () => {
await deleteSecurity({
securityId: security.securityId,
})
.then(() => {
refetch();
utils.application.readTraefikConfig.invalidate({
applicationId,
});
toast.success("Security deleted successfully");
})
.catch(() => {
toast.error("Error deleting security");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
@@ -1,155 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, Pencil } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const UpdateSecuritySchema = z.object({
username: z.string().min(1, "Username is required"),
password: z.string().min(1, "Password is required"),
});
type UpdateSecurity = z.infer<typeof UpdateSecuritySchema>;
interface Props {
securityId: string;
}
export const UpdateSecurity = ({ securityId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { data } = api.security.one.useQuery(
{
securityId,
},
{
enabled: !!securityId,
},
);
const { mutateAsync, isLoading, error, isError } =
api.security.update.useMutation();
const form = useForm<UpdateSecurity>({
defaultValues: {
username: "",
password: "",
},
resolver: zodResolver(UpdateSecuritySchema),
});
useEffect(() => {
if (data) {
form.reset({
username: data.username || "",
password: data.password || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateSecurity) => {
await mutateAsync({
securityId,
username: data.username,
password: data.password,
})
.then(async (response) => {
toast.success("Security Updated");
await utils.application.one.invalidate({
applicationId: response?.applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error updating the security");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Update</DialogTitle>
<DialogDescription>Update the security</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-security"
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="test1" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="test" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-update-security"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -1,297 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
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 React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { InfoIcon } from "lucide-react";
const addResourcesApplication = z.object({
memoryReservation: z.number().nullable().optional(),
cpuLimit: z.number().nullable().optional(),
memoryLimit: z.number().nullable().optional(),
cpuReservation: z.number().nullable().optional(),
});
interface Props {
applicationId: string;
}
type AddResourcesApplication = z.infer<typeof addResourcesApplication>;
export const ShowApplicationResources = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { mutateAsync, isLoading } = api.application.update.useMutation();
const form = useForm<AddResourcesApplication>({
defaultValues: {},
resolver: zodResolver(addResourcesApplication),
});
useEffect(() => {
if (data) {
form.reset({
cpuLimit: data?.cpuLimit || undefined,
cpuReservation: data?.cpuReservation || undefined,
memoryLimit: data?.memoryLimit || undefined,
memoryReservation: data?.memoryReservation || undefined,
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: AddResourcesApplication) => {
await mutateAsync({
applicationId,
cpuLimit: formData.cpuLimit || null,
cpuReservation: formData.cpuReservation || null,
memoryLimit: formData.memoryLimit || null,
memoryReservation: formData.memoryReservation || null,
})
.then(async () => {
toast.success("Resources Updated");
await refetch();
})
.catch(() => {
toast.error("Error updating the resources");
});
};
return (
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Resources</CardTitle>
<CardDescription>
If you want to decrease or increase the resources to a specific.
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<AlertBlock type="info">
Please remember to click Redeploy after modify the resources to apply
the changes.
</AlertBlock>
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="grid w-full md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="268435456 (256MB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="memoryLimit"
render={({ field }) => {
return (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="1073741824 (1GB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="cpuLimit"
render={({ field }) => {
return (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>CPU Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="cpuReservation"
render={({ field }) => {
return (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>CPU Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="1000000000 (1 CPU)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
);
};
@@ -16,42 +16,78 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
TooltipContent,
Tooltip,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addResourcesMysql = z.object({
memoryReservation: z.number().nullable().optional(),
cpuLimit: z.number().nullable().optional(),
memoryLimit: z.number().nullable().optional(),
cpuReservation: z.number().nullable().optional(),
const addResourcesSchema = z.object({
memoryReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
});
export type ServiceType =
| "postgres"
| "mongo"
| "redis"
| "mysql"
| "mariadb"
| "application";
interface Props {
mysqlId: string;
id: string;
type: ServiceType | "application";
}
type AddResourcesMysql = z.infer<typeof addResourcesMysql>;
export const ShowMysqlResources = ({ mysqlId }: Props) => {
const { data, refetch } = api.mysql.one.useQuery(
{
mysqlId,
type AddResources = z.infer<typeof addResourcesSchema>;
export const ShowResources = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddResources>({
defaultValues: {
cpuLimit: "",
cpuReservation: "",
memoryLimit: "",
memoryReservation: "",
},
{ enabled: !!mysqlId },
);
const { mutateAsync, isLoading } = api.mysql.update.useMutation();
const form = useForm<AddResourcesMysql>({
defaultValues: {},
resolver: zodResolver(addResourcesMysql),
resolver: zodResolver(addResourcesSchema),
});
useEffect(() => {
@@ -65,9 +101,14 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
}
}, [data, form, form.reset]);
const onSubmit = async (formData: AddResourcesMysql) => {
const onSubmit = async (formData: AddResources) => {
await mutateAsync({
mysqlId,
mongoId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
applicationId: id || "",
cpuLimit: formData.cpuLimit || null,
cpuReservation: formData.cpuReservation || null,
memoryLimit: formData.memoryLimit || null,
@@ -81,6 +122,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
toast.error("Error updating the resources");
});
};
return (
<Card className="bg-background">
<CardHeader>
@@ -102,50 +144,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
className="grid w-full gap-8 "
>
<div className="grid w-full md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="268435456 (256MB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="memoryLimit"
@@ -172,18 +170,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
<Input
placeholder="1073741824 (1GB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
@@ -191,6 +177,37 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
);
}}
/>
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="268435456 (256MB in bytes)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
@@ -219,17 +236,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
@@ -260,22 +266,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="1000000000 (1 CPU)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
<Input placeholder="1000000000 (1 CPU)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -1,61 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { TrashIcon } from "lucide-react";
import React from "react";
import { toast } from "sonner";
interface Props {
mountId: string;
refetch: () => void;
}
export const DeleteVolume = ({ mountId, refetch }: Props) => {
const { mutateAsync, isLoading } = api.mounts.remove.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the mount
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mountId,
})
.then(() => {
refetch();
toast.success("Mount deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mount");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,4 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -7,40 +9,49 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Package } from "lucide-react";
import { Package, Trash2 } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import type { ServiceType } from "../show-resources";
import { AddVolumes } from "./add-volumes";
import { DeleteVolume } from "./delete-volume";
import { UpdateVolume } from "./update-volume";
interface Props {
applicationId: string;
id: string;
type: ServiceType | "compose";
}
export const ShowVolumes = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
export const ShowVolumes = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
compose: () =>
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { mutateAsync: deleteVolume, isLoading: isRemoving } =
api.mounts.remove.useMutation();
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
<div>
<CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription>
If you want to persist data in this application use the following
config to setup the volumes
If you want to persist data in this service use the following config
to setup the volumes
</CardDescription>
</div>
{data && data?.mounts.length > 0 && (
<AddVolumes
serviceId={applicationId}
refetch={refetch}
serviceType="application"
>
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
Add Volume
</AddVolumes>
)}
@@ -52,17 +63,13 @@ export const ShowVolumes = ({ applicationId }: Props) => {
<span className="text-base text-muted-foreground">
No volumes/mounts configured
</span>
<AddVolumes
serviceId={applicationId}
refetch={refetch}
serviceType="application"
>
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
Add Volume
</AddVolumes>
</div>
) : (
<div className="flex flex-col pt-2 gap-4">
<AlertBlock type="info">
<AlertBlock type="warning">
Please remember to click Redeploy after adding, editing, or
deleting a mount to apply the changes.
</AlertBlock>
@@ -73,7 +80,8 @@ export const ShowVolumes = ({ applicationId }: Props) => {
key={mount.mountId}
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground">
@@ -90,21 +98,12 @@ export const ShowVolumes = ({ applicationId }: Props) => {
)}
{mount.type === "file" && (
<>
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground">
{mount.content}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</span>
</div>
</>
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
{mount.content}
</span>
</div>
)}
{mount.type === "bind" && (
<div className="flex flex-col gap-1">
@@ -114,21 +113,55 @@ export const ShowVolumes = ({ applicationId }: Props) => {
</span>
</div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
{mount.type === "file" ? (
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</span>
</div>
) : (
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
)}
</div>
<div className="flex flex-row gap-1">
<UpdateVolume
mountId={mount.mountId}
type={mount.type}
refetch={refetch}
serviceType="application"
serviceType={type}
/>
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
<DialogAction
title="Delete Volume"
description="Are you sure you want to delete this volume?"
type="destructive"
onClick={async () => {
await deleteVolume({
mountId: mount.mountId,
})
.then(() => {
refetch();
toast.success("Volume deleted successfully");
})
.catch(() => {
toast.error("Error deleting volume");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
@@ -21,7 +21,7 @@ import {
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Pencil } from "lucide-react";
import { PenBoxIcon, Pencil } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -177,8 +177,13 @@ export const UpdateVolume = ({
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<Pencil className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
isLoading={isLoading}
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
@@ -1,161 +0,0 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Copy, TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteApplicationSchema = z.object({
projectName: z.string().min(1, {
message: "Application name is required",
}),
});
type DeleteApplication = z.infer<typeof deleteApplicationSchema>;
interface Props {
applicationId: string;
}
export const DeleteApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.application.delete.useMutation();
const { data } = api.application.one.useQuery(
{ applicationId },
{ enabled: !!applicationId },
);
const { push } = useRouter();
const form = useForm<DeleteApplication>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteApplicationSchema),
});
const onSubmit = async (formData: DeleteApplication) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({
applicationId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Application deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the application");
});
} else {
form.setError("projectName", {
message: "Project name does not match",
});
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
application. If you are sure please enter the application name to
delete this application.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-application"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<span>
To confirm, type{" "}
<Badge
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
variant="outline"
onClick={() => {
if (data?.name && data?.appName) {
navigator.clipboard.writeText(
`${data.name}/${data.appName}`,
);
toast.success("Copied to clipboard. Be careful!");
}
}}
>
{data?.name}/{data?.appName}&nbsp;
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
</Badge>{" "}
in the box below:
</span>
</FormLabel>
<FormControl>
<Input
placeholder="Enter application name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-application"
type="submit"
variant="destructive"
>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -20,6 +20,12 @@ interface Props {
export const CancelQueues = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
const { data: isCloud } = api.settings.isCloud.useQuery();
if (isCloud) {
return null;
}
return (
<AlertDialog>
<AlertDialogTrigger asChild>
@@ -17,8 +17,15 @@ interface Props {
open: boolean;
onClose: () => void;
serverId?: string;
errorMessage?: string;
}
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
export const ShowDeployment = ({
logPath,
open,
onClose,
serverId,
errorMessage,
}: Props) => {
const [data, setData] = useState("");
const [showExtraLogs, setShowExtraLogs] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
@@ -99,6 +106,8 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
}
}, [filteredLogs, autoScroll]);
const optionalErrors = parseLogs(errorMessage || "");
return (
<Dialog
open={open}
@@ -157,9 +166,17 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
<TerminalLine key={index} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
<>
{optionalErrors.length > 0 ? (
optionalErrors.map((log: LogLine, index: number) => (
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
</>
)}
</div>
</DialogContent>
@@ -8,7 +8,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueues } from "./cancel-queues";
@@ -18,8 +18,11 @@ import { ShowDeployment } from "./show-deployment";
interface Props {
applicationId: string;
}
export const ShowDeployments = ({ applicationId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const { data } = api.application.one.useQuery({ applicationId });
const { data: deployments } = api.deployment.all.useQuery(
{ applicationId },
@@ -100,7 +103,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -112,9 +115,10 @@ export const ShowDeployments = ({ applicationId }: Props) => {
)}
<ShowDeployment
serverId={data?.serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</CardContent>
</Card>
@@ -104,9 +104,7 @@ export const AddDomain = ({
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error updating the domain"
: "Error creating the domain",
error: domainId ? "Error updating the domain" : "Error creating the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
@@ -1,73 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { TrashIcon } from "lucide-react";
import React from "react";
import { toast } from "sonner";
interface Props {
domainId: string;
}
export const DeleteDomain = ({ domainId }: Props) => {
const { mutateAsync, isLoading } = api.domain.delete.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
domain
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
domainId,
})
.then((data) => {
if (data?.applicationId) {
utils.domain.byApplicationId.invalidate({
applicationId: data?.applicationId,
});
utils.application.readTraefikConfig.invalidate({
applicationId: data?.applicationId,
});
} else if (data?.composeId) {
utils.domain.byComposeId.invalidate({
composeId: data?.composeId,
});
}
toast.success("Domain delete successfully");
})
.catch(() => {
toast.error("Error deleting the Domain");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,3 +1,4 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -6,19 +7,18 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { AddDomain } from "./add-domain";
import { DeleteDomain } from "./delete-domain";
interface Props {
applicationId: string;
}
export const ShowDomains = ({ applicationId }: Props) => {
const { data } = api.domain.byApplicationId.useQuery(
const { data, refetch } = api.domain.byApplicationId.useQuery(
{
applicationId,
},
@@ -26,6 +26,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
enabled: !!applicationId,
},
);
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
api.domain.delete.useMutation();
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
@@ -69,35 +73,66 @@ export const ShowDomains = ({ applicationId }: Props) => {
return (
<div
key={item.domainId}
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
className="flex w-full items-center justify-between gap-4 border p-4 md:px-6 rounded-lg flex-wrap"
>
<Link
className="md:basis-1/2 flex gap-2 items-center hover:underline transition-all w-full"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
<ExternalLink className="size-5" />
<span className="truncate max-w-full text-sm">
{item.host}
</span>
<ExternalLink className="size-4 min-w-4" />
</Link>
<Input disabled value={item.host} />
<Button variant="outline" disabled>
{item.path}
</Button>
<Button variant="outline" disabled>
{item.port}
</Button>
<Button variant="outline" disabled>
{item.https ? "HTTPS" : "HTTP"}
</Button>
<div className="flex flex-row gap-1">
<AddDomain
applicationId={applicationId}
domainId={item.domainId}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</AddDomain>
<DeleteDomain domainId={item.domainId} />
<div className="flex gap-8">
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
<span>{item.path}</span>
<span>{item.port}</span>
<span>{item.https ? "HTTPS" : "HTTP"}</span>
</div>
<div className="flex gap-2">
<AddDomain
applicationId={applicationId}
domainId={item.domainId}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</AddDomain>
<DialogAction
title="Delete Domain"
description="Are you sure you want to delete this domain?"
type="destructive"
onClick={async () => {
await deleteDomain({
domainId: item.domainId,
})
.then(() => {
refetch();
toast.success("Domain deleted successfully");
})
.catch(() => {
toast.error("Error deleting domain");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
);
@@ -18,10 +18,11 @@ import { Toggle } from "@/components/ui/toggle";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import React, { type CSSProperties, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { ServiceType } from "../advanced/show-resources";
const addEnvironmentSchema = z.object({
environment: z.string(),
@@ -30,21 +31,39 @@ const addEnvironmentSchema = z.object({
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
interface Props {
mariadbId: string;
id: string;
type: Exclude<ServiceType | "compose", "application">;
}
export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
export const ShowEnvironment = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
compose: () =>
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const [isEnvVisible, setIsEnvVisible] = useState(true);
const { mutateAsync, isLoading } = api.mariadb.saveEnvironment.useMutation();
const { data, refetch } = api.mariadb.one.useQuery(
{
mariadbId,
},
{
enabled: !!mariadbId,
},
);
const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
compose: () => api.compose.update.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<EnvironmentSchema>({
defaultValues: {
environment: "",
@@ -62,8 +81,13 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
const onSubmit = async (data: EnvironmentSchema) => {
mutateAsync({
mongoId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
composeId: id || "",
env: data.environment,
mariadbId,
})
.then(async () => {
toast.success("Environments Added");
@@ -77,7 +101,6 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
{" "}
<CardHeader className="flex flex-row w-full items-center justify-between">
<div>
<CardTitle className="text-xl">Environment Settings</CardTitle>
@@ -112,6 +135,11 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
<FormItem className="w-full">
<FormControl>
<CodeEditor
style={
{
WebkitTextSecurity: isEnvVisible ? "disc" : null,
} as CSSProperties
}
language="properties"
disabled={isEnvVisible}
placeholder={`NODE_ENV=production
@@ -56,12 +56,12 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-full flex-col gap-5 "
>
<Card className="bg-background p-6">
<Card className="bg-background px-6 pb-6">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-full flex-col gap-4"
>
<Secrets
name="env"
title="Environment Settings"
@@ -89,15 +89,13 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
placeholder="NPM_TOKEN=xyz"
/>
)}
<CardContent>
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">
Save
</Button>
</div>
</CardContent>
</Card>
</form>
</Form>
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">
Save
</Button>
</div>
</form>
</Form>
</Card>
);
};
@@ -1,74 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { useRouter } from "next/router";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const DeployApplication = ({ applicationId }: Props) => {
const router = useRouter();
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { mutateAsync: deploy } = api.application.deploy.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button isLoading={data?.applicationStatus === "running"}>
Deploy
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will deploy the application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await deploy({
applicationId,
})
.then(async () => {
toast.success("Application deployed successfully");
await refetch();
router.push(
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
);
})
.catch(() => {
toast.error("Error deploying the Application");
});
await refetch();
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -235,7 +235,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -245,7 +245,12 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
@@ -226,7 +226,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -236,7 +236,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.login}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
@@ -248,7 +248,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -260,7 +260,12 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
@@ -1,70 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import { toast } from "sonner";
interface Props {
applicationId: string;
appName: string;
}
export const ResetApplication = ({ applicationId, appName }: Props) => {
const { refetch } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { mutateAsync: reload, isLoading } =
api.application.reload.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will reload the application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await reload({
applicationId,
appName,
})
.then(() => {
toast.success("Service Reloaded");
})
.catch(() => {
toast.error("Error reloading the service");
});
await refetch();
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,23 +1,21 @@
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { Terminal } from "lucide-react";
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import React from "react";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
import { RedbuildApplication } from "../rebuild-application";
import { StartApplication } from "../start-application";
import { StopApplication } from "../stop-application";
import { DeployApplication } from "./deploy-application";
import { ResetApplication } from "./reset-application";
interface Props {
applicationId: string;
}
export const ShowGeneralApplication = ({ applicationId }: Props) => {
const router = useRouter();
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
@@ -25,6 +23,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync: update } = api.application.update.useMutation();
const { mutateAsync: start, isLoading: isStarting } =
api.application.start.useMutation();
const { mutateAsync: stop, isLoading: isStopping } =
api.application.stop.useMutation();
const { mutateAsync: deploy, isLoading: isDeploying } =
api.application.deploy.useMutation();
const { mutateAsync: reload, isLoading: isReloading } =
api.application.reload.useMutation();
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
return (
<>
@@ -33,17 +43,127 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<DeployApplication applicationId={applicationId} />
<ResetApplication
applicationId={applicationId}
appName={data?.appName || ""}
/>
<DialogAction
title="Deploy Application"
description="Are you sure you want to deploy this application?"
type="default"
onClick={async () => {
await deploy({
applicationId: applicationId,
})
.then(() => {
toast.success("Application deployed successfully");
refetch();
router.push(
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
);
})
.catch(() => {
toast.error("Error deploying application");
});
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
>
Deploy
</Button>
</DialogAction>
<DialogAction
title="Reload Application"
description="Are you sure you want to reload this application?"
type="default"
onClick={async () => {
await reload({
applicationId: applicationId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Application reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading application");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
<DialogAction
title="Rebuild Application"
description="Are you sure you want to rebuild this application?"
type="default"
onClick={async () => {
await redeploy({
applicationId: applicationId,
})
.then(() => {
toast.success("Application rebuilt successfully");
refetch();
})
.catch(() => {
toast.error("Error rebuilding application");
});
}}
>
<Button
variant="secondary"
isLoading={data?.applicationStatus === "running"}
>
Rebuild
<Hammer className="size-4" />
</Button>
</DialogAction>
<RedbuildApplication applicationId={applicationId} />
{data?.applicationStatus === "idle" ? (
<StartApplication applicationId={applicationId} />
<DialogAction
title="Start Application"
description="Are you sure you want to start this application?"
type="default"
onClick={async () => {
await start({
applicationId: applicationId,
})
.then(() => {
toast.success("Application started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting application");
});
}}
>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
) : (
<StopApplication applicationId={applicationId} />
<DialogAction
title="Stop Application"
description="Are you sure you want to stop this application?"
onClick={async () => {
await stop({
applicationId: applicationId,
})
.then(() => {
toast.success("Application stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping application");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
appName={data?.appName || ""}
@@ -104,9 +104,7 @@ export const AddPreviewDomain = ({
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error updating the domain"
: "Error creating the domain",
error: domainId ? "Error updating the domain" : "Error creating the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
@@ -26,7 +26,9 @@ export const ShowPreviewBuilds = ({
serverId,
trigger,
}: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -77,7 +79,7 @@ export const ShowPreviewBuilds = ({
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -89,9 +91,10 @@ export const ShowPreviewBuilds = ({
</DialogContent>
<ShowDeployment
serverId={serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</Dialog>
);
@@ -291,16 +291,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
"PORT=3000",
].join("\n")}
/>
{/* <CodeEditor
lineWrapping
language="properties"
wrapperClassName="h-[25rem] font-mono"
placeholder={`NODE_ENV=production
PORT=3000
`}
{...field}
/> */}
</FormControl>
<FormMessage />
</FormItem>
@@ -1,76 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Hammer } from "lucide-react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const RedbuildApplication = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { mutateAsync } = api.application.redeploy.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
isLoading={data?.applicationStatus === "running"}
>
Rebuild
<Hammer className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to rebuild the application?
</AlertDialogTitle>
<AlertDialogDescription>
Is required to deploy at least 1 time in order to reuse the same
code
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
toast.success("Redeploying Application....");
await mutateAsync({
applicationId,
})
.then(async () => {
await utils.application.one.invalidate({
applicationId,
});
})
.catch(() => {
toast.error("Error rebuilding the application");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,65 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const StartApplication = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.application.start.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Start
<CheckCircle2 className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to start the application?
</AlertDialogTitle>
<AlertDialogDescription>
This will start the application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then(async () => {
await utils.application.one.invalidate({
applicationId,
});
toast.success("Application started successfully");
})
.catch(() => {
toast.error("Error starting the Application");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,65 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Ban } from "lucide-react";
import { toast } from "sonner";
interface Props {
applicationId: string;
}
export const StopApplication = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } = api.application.stop.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Stop
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure to stop the application?
</AlertDialogTitle>
<AlertDialogDescription>
This will stop the application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then(async () => {
await utils.application.one.invalidate({
applicationId,
});
toast.success("Application stopped successfully");
})
.catch(() => {
toast.error("Error stopping the Application");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } from "lucide-react";
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -91,8 +91,12 @@ export const UpdateApplication = ({ applicationId }: Props) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
@@ -1,142 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Package } from "lucide-react";
import React from "react";
import { AddVolumes } from "../../application/advanced/volumes/add-volumes";
import { DeleteVolume } from "../../application/advanced/volumes/delete-volume";
import { UpdateVolume } from "../../application/advanced/volumes/update-volume";
interface Props {
composeId: string;
}
export const ShowVolumesCompose = ({ composeId }: Props) => {
const { data, refetch } = api.compose.one.useQuery(
{
composeId,
},
{ enabled: !!composeId },
);
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
<div>
<CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription>
If you want to persist data in this compose use the following config
to setup the volumes
</CardDescription>
</div>
{data && data?.mounts.length > 0 && (
<AddVolumes
serviceId={composeId}
refetch={refetch}
serviceType="compose"
>
Add Volume
</AddVolumes>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data?.mounts.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<Package className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No volumes/mounts configured
</span>
<AddVolumes
serviceId={composeId}
refetch={refetch}
serviceType="compose"
>
Add Volume
</AddVolumes>
</div>
) : (
<div className="flex flex-col pt-2 gap-4">
<AlertBlock type="info">
Please remember to click Redeploy after adding, editing, or
deleting a mount to apply the changes.
</AlertBlock>
<div className="flex flex-col gap-6">
{data?.mounts.map((mount) => (
<div key={mount.mountId}>
<div
key={mount.mountId}
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground">
{mount.type.toUpperCase()}
</span>
</div>
{mount.type === "volume" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Volume Name</span>
<span className="text-sm text-muted-foreground">
{mount.volumeName}
</span>
</div>
)}
{mount.type === "file" && (
<>
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground w-40 truncate">
{mount.content}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</span>
</div>
</>
)}
{mount.type === "bind" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Host Path</span>
<span className="text-sm text-muted-foreground">
{mount.hostPath}
</span>
</div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
</div>
<div className="flex flex-row gap-1">
<UpdateVolume
mountId={mount.mountId}
type={mount.type}
refetch={refetch}
serviceType="compose"
/>
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
};
@@ -13,7 +13,6 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -21,9 +20,10 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import type { ServiceType } from "@dokploy/server/db/schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { Copy } from "lucide-react";
import { TrashIcon } from "lucide-react";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -40,16 +40,42 @@ const deleteComposeSchema = z.object({
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
interface Props {
composeId: string;
id: string;
type: ServiceType | "application";
}
export const DeleteCompose = ({ composeId }: Props) => {
export const DeleteService = ({ id, type }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
const { data } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
compose: () =>
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const mutationMap = {
postgres: () => api.postgres.remove.useMutation(),
redis: () => api.redis.remove.useMutation(),
mysql: () => api.mysql.remove.useMutation(),
mariadb: () => api.mariadb.remove.useMutation(),
application: () => api.application.delete.useMutation(),
mongo: () => api.mongo.remove.useMutation(),
compose: () => api.compose.delete.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.remove.useMutation();
const { push } = useRouter();
const form = useForm<DeleteCompose>({
defaultValues: {
@@ -63,14 +89,23 @@ export const DeleteCompose = ({ composeId }: Props) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
const { deleteVolumes } = formData;
await mutateAsync({ composeId, deleteVolumes })
await mutateAsync({
mongoId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
applicationId: id || "",
composeId: id || "",
deleteVolumes,
})
.then((result) => {
push(`/dashboard/project/${result?.projectId}`);
toast.success("Compose deleted successfully");
toast.success("deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the compose");
toast.error("Error deleting the service");
});
} else {
form.setError("projectName", {
@@ -82,8 +117,13 @@ export const DeleteCompose = ({ composeId }: Props) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isLoading}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
@@ -91,8 +131,8 @@ export const DeleteCompose = ({ composeId }: Props) => {
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
compose. If you are sure please enter the compose name to delete
this compose.
service. If you are sure please enter the service name to delete
this service.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
@@ -115,9 +155,7 @@ export const DeleteCompose = ({ composeId }: Props) => {
variant="outline"
onClick={() => {
if (data?.name && data?.appName) {
navigator.clipboard.writeText(
`${data.name}/${data.appName}`,
);
copy(`${data.name}/${data.appName}`);
toast.success("Copied to clipboard. Be careful!");
}
}}
@@ -138,27 +176,29 @@ export const DeleteCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="deleteVolumes"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
{type === "compose" && (
<FormField
control={form.control}
name="deleteVolumes"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="ml-2">
Delete volumes associated with this compose
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormLabel className="ml-2">
Delete volumes associated with this compose
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
</div>
@@ -20,6 +20,11 @@ interface Props {
export const CancelQueuesCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
const { data: isCloud } = api.settings.isCloud.useQuery();
if (isCloud) {
return null;
}
return (
<AlertDialog>
<AlertDialogTrigger asChild>
@@ -17,12 +17,14 @@ interface Props {
serverId?: string;
open: boolean;
onClose: () => void;
errorMessage?: string;
}
export const ShowDeploymentCompose = ({
logPath,
open,
onClose,
serverId,
errorMessage,
}: Props) => {
const [data, setData] = useState("");
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
@@ -105,6 +107,8 @@ export const ShowDeploymentCompose = ({
}
}, [filteredLogs, autoScroll]);
const optionalErrors = parseLogs(errorMessage || "");
return (
<Dialog
open={open}
@@ -161,9 +165,17 @@ export const ShowDeploymentCompose = ({
<TerminalLine key={index} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
<>
{optionalErrors.length > 0 ? (
optionalErrors.map((log: LogLine, index: number) => (
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
</>
)}
</div>
</DialogContent>
@@ -8,7 +8,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueuesCompose } from "./cancel-queues-compose";
@@ -19,7 +19,9 @@ interface Props {
composeId: string;
}
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const { data } = api.compose.one.useQuery({ composeId });
const { data: deployments } = api.deployment.allByCompose.useQuery(
{ composeId },
@@ -100,7 +102,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -112,9 +114,10 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
)}
<ShowDeploymentCompose
serverId={data?.serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</CardContent>
</Card>
@@ -126,9 +126,7 @@ export const AddDomainCompose = ({
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error updating the domain"
: "Error creating the domain",
error: domainId ? "Error updating the domain" : "Error creating the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
@@ -1,3 +1,4 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -6,11 +7,10 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
import Link from "next/link";
import { DeleteDomain } from "../../application/domains/delete-domain";
import { toast } from "sonner";
import { AddDomainCompose } from "./add-domain";
interface Props {
@@ -18,7 +18,7 @@ interface Props {
}
export const ShowDomainsCompose = ({ composeId }: Props) => {
const { data } = api.domain.byComposeId.useQuery(
const { data, refetch } = api.domain.byComposeId.useQuery(
{
composeId,
},
@@ -27,6 +27,9 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
},
);
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
api.domain.delete.useMutation();
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
@@ -70,34 +73,70 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
return (
<div
key={item.domainId}
className="flex w-full items-center gap-4 max-sm:flex-wrap border p-4 rounded-lg"
className="flex w-full items-center justify-between gap-4 border p-4 md:px-6 rounded-lg flex-wrap"
>
<Link target="_blank" href={`http://${item.host}`}>
<ExternalLink className="size-5" />
</Link>
<Button variant="outline" disabled>
{item.serviceName}
</Button>
<Input disabled value={item.host} />
<Button variant="outline" disabled>
{item.path}
</Button>
<Button variant="outline" disabled>
{item.port}
</Button>
<Button variant="outline" disabled>
{item.https ? "HTTPS" : "HTTP"}
</Button>
<div className="flex flex-row gap-1">
<AddDomainCompose
composeId={composeId}
domainId={item.domainId}
<div className="md:basis-1/2 flex gap-6 w-full items-center">
<span className="opacity-50 text-center font-medium text-sm whitespace-nowrap">
{item.serviceName}
</span>
<Link
className="flex gap-2 items-center hover:underline transition-all w-full max-w-[calc(100%-4rem)]"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</AddDomainCompose>
<DeleteDomain domainId={item.domainId} />
<span className="truncate text-sm">{item.host}</span>
<ExternalLink className="size-4 min-w-4" />
</Link>
</div>
<div className="flex gap-8">
<div className="flex gap-8 opacity-50 items-center h-10 text-center text-sm font-medium">
<span>{item.path}</span>
<span>{item.port}</span>
<span>{item.https ? "HTTPS" : "HTTP"}</span>
</div>
<div className="flex gap-2">
<AddDomainCompose
composeId={composeId}
domainId={item.domainId}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</AddDomainCompose>
<DialogAction
title="Delete Domain"
description="Are you sure you want to delete this domain?"
type="destructive"
onClick={async () => {
await deleteDomain({
domainId: item.domainId,
})
.then((data) => {
refetch();
toast.success("Domain deleted successfully");
})
.catch(() => {
toast.error("Error deleting domain");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
);
@@ -1,167 +0,0 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Toggle } from "@/components/ui/toggle";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addEnvironmentSchema = z.object({
environment: z.string(),
});
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
interface Props {
composeId: string;
}
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
const [isEnvVisible, setIsEnvVisible] = useState(true);
const { mutateAsync, isLoading } = api.compose.update.useMutation();
const { data, refetch } = api.compose.one.useQuery(
{
composeId,
},
{
enabled: !!composeId,
},
);
const form = useForm<EnvironmentSchema>({
defaultValues: {
environment: "",
},
resolver: zodResolver(addEnvironmentSchema),
});
useEffect(() => {
if (data) {
form.reset({
environment: data.env || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: EnvironmentSchema) => {
mutateAsync({
env: data.environment,
composeId,
})
.then(async () => {
toast.success("Environments Added");
await refetch();
})
.catch(() => {
toast.error("Error adding environment");
});
};
useEffect(() => {
if (isEnvVisible) {
if (data?.env) {
const maskedLines = data.env
.split("\n")
.map((line) => "*".repeat(line.length))
.join("\n");
form.reset({
environment: maskedLines,
});
} else {
form.reset({
environment: "",
});
}
} else {
form.reset({
environment: data?.env || "",
});
}
}, [form.reset, data, form, isEnvVisible]);
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader className="flex flex-row w-full items-center justify-between">
<div>
<CardTitle className="text-xl">Environment Settings</CardTitle>
<CardDescription>
You can add environment variables to your resource.
</CardDescription>
</div>
<Toggle
aria-label="Toggle bold"
pressed={isEnvVisible}
onPressedChange={setIsEnvVisible}
>
{isEnvVisible ? (
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
) : (
<EyeIcon className="h-4 w-4 text-muted-foreground" />
)}
</Toggle>
</CardHeader>
<CardContent>
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full space-y-4"
>
<FormField
control={form.control}
name="environment"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<CodeEditor
language="properties"
disabled={isEnvVisible}
placeholder={`NODE_ENV=production
PORT=3000
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row justify-end">
<Button
disabled={isEnvVisible}
isLoading={isLoading}
className="w-fit"
type="submit"
>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
);
};
@@ -1,28 +1,17 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
import Link from "next/link";
import { Ban, CheckCircle2, Hammer, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
import { StartCompose } from "../start-compose";
import { DeployCompose } from "./deploy-compose";
import { RedbuildCompose } from "./rebuild-compose";
import { StopCompose } from "./stop-compose";
interface Props {
composeId: string;
}
export const ComposeActions = ({ composeId }: Props) => {
const router = useRouter();
const { data, refetch } = api.compose.one.useQuery(
{
composeId,
@@ -30,33 +19,109 @@ export const ComposeActions = ({ composeId }: Props) => {
{ enabled: !!composeId },
);
const { mutateAsync: update } = api.compose.update.useMutation();
const extractDomains = (env: string) => {
const lines = env.split("\n");
const hostLines = lines.filter((line) => {
const [key, value] = line.split("=");
return key?.trim().endsWith("_HOST");
});
const hosts = hostLines.map((line) => {
const [key, value] = line.split("=");
return value ? value.trim() : "";
});
return hosts;
};
const domains = extractDomains(data?.env || "");
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
const { mutateAsync: start, isLoading: isStarting } =
api.compose.start.useMutation();
const { mutateAsync: stop, isLoading: isStopping } =
api.compose.stop.useMutation();
return (
<div className="flex flex-row gap-4 w-full flex-wrap ">
<DeployCompose composeId={composeId} />
<RedbuildCompose composeId={composeId} />
<DialogAction
title="Deploy Compose"
description="Are you sure you want to deploy this compose?"
type="default"
onClick={async () => {
await deploy({
composeId: composeId,
})
.then(() => {
toast.success("Compose deployed successfully");
refetch();
router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
);
})
.catch(() => {
toast.error("Error deploying compose");
});
}}
>
<Button variant="default" isLoading={data?.composeStatus === "running"}>
Deploy
</Button>
</DialogAction>
<DialogAction
title="Rebuild Compose"
description="Are you sure you want to rebuild this compose?"
type="default"
onClick={async () => {
await redeploy({
composeId: composeId,
})
.then(() => {
toast.success("Compose rebuilt successfully");
refetch();
})
.catch(() => {
toast.error("Error rebuilding compose");
});
}}
>
<Button
variant="secondary"
isLoading={data?.composeStatus === "running"}
>
Rebuild
<Hammer className="size-4" />
</Button>
</DialogAction>
{data?.composeType === "docker-compose" &&
data?.composeStatus === "idle" ? (
<StartCompose composeId={composeId} />
<DialogAction
title="Start Compose"
description="Are you sure you want to start this compose?"
type="default"
onClick={async () => {
await start({
composeId: composeId,
})
.then(() => {
toast.success("Compose started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting compose");
});
}}
>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
) : (
<StopCompose composeId={composeId} />
<DialogAction
title="Stop Compose"
description="Are you sure you want to stop this compose?"
onClick={async () => {
await stop({
composeId: composeId,
})
.then(() => {
toast.success("Compose stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping compose");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
@@ -89,41 +154,6 @@ export const ComposeActions = ({ composeId }: Props) => {
className="flex flex-row gap-2 items-center"
/>
</div>
{domains.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
Domains
<Globe className="text-xs size-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Domains detected</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{domains.map((host, index) => {
const url =
host.startsWith("http://") || host.startsWith("https://")
? host
: `http://${host}`;
return (
<DropdownMenuItem
key={`domain-${index}`}
className="cursor-pointer"
asChild
>
<Link href={url} target="_blank">
{host}
<ExternalLink className="ml-2 text-xs text-muted-foreground" />
</Link>
</DropdownMenuItem>
);
})}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
};
@@ -1,72 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { useRouter } from "next/router";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const DeployCompose = ({ composeId }: Props) => {
const router = useRouter();
const { data, refetch } = api.compose.one.useQuery(
{
composeId,
},
{ enabled: !!composeId },
);
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button isLoading={data?.composeStatus === "running"}>Deploy</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will deploy the compose
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
toast.success("Deploying Compose....");
await refetch();
await deploy({
composeId,
})
.then(async () => {
router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
);
})
.catch(() => {
toast.error("Error deploying Compose");
});
await refetch();
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -237,7 +237,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -247,7 +247,12 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
@@ -228,7 +228,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -238,7 +238,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.login}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
@@ -250,7 +250,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -262,7 +262,12 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
@@ -1,75 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Hammer } from "lucide-react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const RedbuildCompose = ({ composeId }: Props) => {
const { data } = api.compose.one.useQuery(
{
composeId,
},
{ enabled: !!composeId },
);
const { mutateAsync } = api.compose.redeploy.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
isLoading={data?.composeStatus === "running"}
>
Rebuild
<Hammer className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to rebuild the compose?
</AlertDialogTitle>
<AlertDialogDescription>
Is required to deploy at least 1 time in order to reuse the same
code
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
toast.success("Redeploying Compose....");
await mutateAsync({
composeId,
})
.then(async () => {
await utils.compose.one.invalidate({
composeId,
});
})
.catch(() => {
toast.error("Error rebuilding the compose");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,69 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Ban } from "lucide-react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const StopCompose = ({ composeId }: Props) => {
const { data } = api.compose.one.useQuery(
{
composeId,
},
{ enabled: !!composeId },
);
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Stop
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure to stop the compose?</AlertDialogTitle>
<AlertDialogDescription>
This will stop the compose services
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then(async () => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose stopped successfully");
})
.catch(() => {
toast.error("Error stopping the compose");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,65 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const StartCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.start.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Start
<CheckCircle2 className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to start the compose?
</AlertDialogTitle>
<AlertDialogDescription>
This will start the compose
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then(async () => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose started successfully");
})
.catch(() => {
toast.error("Error starting the Compose");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,65 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Ban } from "lucide-react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const StopCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Stop
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure to stop the compose?
</AlertDialogTitle>
<AlertDialogDescription>
This will stop the compose
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then(async () => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose stopped successfully");
})
.catch(() => {
toast.error("Error stopping the Compose");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { SquarePen } from "lucide-react";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -91,8 +91,12 @@ export const UpdateCompose = ({ composeId }: Props) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
@@ -1,62 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { TrashIcon } from "lucide-react";
import { toast } from "sonner";
interface Props {
backupId: string;
refetch: () => void;
}
export const DeleteBackup = ({ backupId, refetch }: Props) => {
const { mutateAsync, isLoading } = api.backup.remove.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
backup
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
backupId,
})
.then(() => {
refetch();
toast.success("Backup deleted successfully");
})
.catch(() => {
toast.error("Error deleting the backup");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,3 +1,4 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -13,31 +14,47 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { DatabaseBackup, Play } from "lucide-react";
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
import Link from "next/link";
import React from "react";
import { toast } from "sonner";
import { AddBackup } from "../../database/backups/add-backup";
import { DeleteBackup } from "../../database/backups/delete-backup";
import { UpdateBackup } from "../../database/backups/update-backup";
import type { ServiceType } from "../../application/advanced/show-resources";
import { AddBackup } from "./add-backup";
import { UpdateBackup } from "./update-backup";
interface Props {
postgresId: string;
id: string;
type: Exclude<ServiceType, "application" | "redis">;
}
export const ShowBackupPostgres = ({ postgresId }: Props) => {
export const ShowBackups = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data } = api.destination.all.useQuery();
const { data: postgres, refetch: refetchPostgres } =
api.postgres.one.useQuery(
{
postgresId,
},
{
enabled: !!postgresId,
},
);
const { data: postgres, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { mutateAsync: manualBackup, isLoading: isManualBackup } =
api.backup.manualBackupPostgres.useMutation();
const mutationMap = {
postgres: () => api.backup.manualBackupPostgres.useMutation(),
mysql: () => api.backup.manualBackupMySql.useMutation(),
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
mongo: () => api.backup.manualBackupMongo.useMutation(),
};
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
type
]
? mutationMap[type]()
: api.backup.manualBackupMongo.useMutation();
const { mutateAsync: deleteBackup, isLoading: isRemoving } =
api.backup.remove.useMutation();
return (
<Card className="bg-background">
@@ -51,25 +68,21 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
</div>
{postgres && postgres?.backups?.length > 0 && (
<AddBackup
databaseId={postgresId}
databaseType="postgres"
refetch={refetchPostgres}
/>
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
<span className="text-base text-muted-foreground text-center">
To create a backup it is required to set at least 1 provider.
Please, go to{" "}
<Link
href="/dashboard/settings/server"
href="/dashboard/settings/destinations"
className="text-foreground"
>
Settings
S3 Destinations
</Link>{" "}
to do so.
</span>
@@ -83,9 +96,9 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
No backups configured
</span>
<AddBackup
databaseId={postgresId}
databaseType="postgres"
refetch={refetchPostgres}
databaseId={id}
databaseType={type}
refetch={refetch}
/>
</div>
) : (
@@ -158,12 +171,34 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
</TooltipProvider>
<UpdateBackup
backupId={backup.backupId}
refetch={refetchPostgres}
/>
<DeleteBackup
backupId={backup.backupId}
refetch={refetchPostgres}
refetch={refetch}
/>
<DialogAction
title="Delete Backup"
description="Are you sure you want to delete this backup?"
type="destructive"
onClick={async () => {
await deleteBackup({
backupId: backup.backupId,
})
.then(() => {
refetch();
toast.success("Backup deleted successfully");
})
.catch(() => {
toast.error("Error deleting backup");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>
@@ -116,8 +116,12 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
@@ -226,7 +226,7 @@ export const DockerLogsId: React.FC<Props> = ({
return (
<div className="flex flex-col gap-4">
<div className="rounded-lg overflow-hidden">
<div className="rounded-lg">
<div className="space-y-4">
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
<div className="flex flex-wrap gap-4">
@@ -43,7 +43,7 @@ const LOG_STYLES: Record<LogType, LogStyle> = {
export function parseLogs(logString: string): LogLine[] {
// Regex to match the log line format
// Exemple of return :
// Example of return :
// 1 2024-12-10T10:00:00.000Z The server is running on port 8080
// Should return :
// { timestamp: new Date("2024-12-10T10:00:00.000Z"),
@@ -63,18 +63,10 @@ export function parseLogs(logString: string): LogLine[] {
if (!message?.trim()) return null;
// Delete other timestamps and keep only the one from --timestamps
const cleanedMessage = message
?.replace(
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g,
"",
)
.trim();
return {
rawTimestamp: timestamp ?? null,
timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
message: cleanedMessage,
message: message.trim(),
};
})
.filter((log) => log !== null);
@@ -9,10 +9,18 @@ import {
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ChevronDown } from "lucide-react";
import { ChevronDown, Container } from "lucide-react";
import * as React from "react";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -71,139 +79,164 @@ export const ShowContainers = ({ serverId }: Props) => {
});
return (
<div className="mt-6 grid gap-4 pb-20 w-full">
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex items-center gap-2 max-sm:flex-wrap">
<Input
placeholder="Filter by name..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("name")?.setFilterValue(event.target.value)
}
className="md:max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="sm:ml-auto max-sm:w-full">
Columns <ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : data?.length === 0 ? (
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
No results.
</span>
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<Container className="size-6 text-muted-foreground self-center" />
Docker Containers
</CardTitle>
<CardDescription>
See all the containers of your dokploy server
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
<div className="gap-4 pb-20 w-full">
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex items-center gap-2 max-sm:flex-wrap">
<Input
placeholder="Filter by name..."
value={
(table.getColumn("name")?.getFilterValue() as string) ??
""
}
onChange={(event) =>
table
.getColumn("name")
?.setFilterValue(event.target.value)
}
className="md:max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="sm:ml-auto max-sm:w-full"
>
Columns <ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : data?.length === 0 ? (
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
No results.
</span>
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table?.getRowModel()?.rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : (
<>No results.</>
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table?.getRowModel()?.rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : (
<>No results.</>
)}
</TableCell>
</TableRow>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
{data && data?.length > 0 && (
<div className="flex items-center justify-end space-x-2 py-4">
<div className="space-x-2 flex flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)}
</TableBody>
</Table>
)}
</div>
{data && data?.length > 0 && (
<div className="flex items-center justify-end space-x-2 py-4">
<div className="space-x-2 flex flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
)}
</div>
</CardContent>
</div>
</Card>
</div>
);
};
@@ -1,8 +1,15 @@
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tree } from "@/components/ui/file-tree";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
import { FileIcon, Folder, Link, Loader2, Workflow } from "lucide-react";
import React from "react";
import { ShowTraefikFile } from "./show-traefik-file";
@@ -27,53 +34,77 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
);
return (
<div className={cn("mt-6 md:grid gap-4")}>
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
{isError && (
<AlertBlock type="error" className="w-full">
{error?.message}
</AlertBlock>
)}
{isLoading && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
<Loader2 className="animate-spin size-8 text-muted-foreground" />
</div>
)}
{directories?.length === 0 && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
No directories or files detected in {"'/etc/dokploy/traefik'"}
</span>
<Folder className="size-8 text-muted-foreground" />
</div>
)}
{directories && directories?.length > 0 && (
<>
<Tree
data={directories}
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg"
onSelectChange={(item) => setFile(item?.id || null)}
folderIcon={Folder}
itemIcon={Workflow}
/>
<div className="w-full">
{file ? (
<ShowTraefikFile path={file} serverId={serverId} />
) : (
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
<span className="text-muted-foreground text-lg font-medium">
No file selected
</span>
<FileIcon className="size-8 text-muted-foreground" />
</div>
)}
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<FileIcon className="size-6 text-muted-foreground self-center" />
Traefik File System
</CardTitle>
<CardDescription>
Manage all the files and directories in {"'/etc/dokploy/traefik'"}
.
</CardDescription>
<AlertBlock type="warning">
Adding invalid configuration to existing files, can break your
Traefik instance, preventing access to your applications.
</AlertBlock>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
<div>
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
{isError && (
<AlertBlock type="error" className="w-full">
{error?.message}
</AlertBlock>
)}
{isLoading && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
<Loader2 className="animate-spin size-8 text-muted-foreground" />
</div>
)}
{directories?.length === 0 && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
No directories or files detected in{" "}
{"'/etc/dokploy/traefik'"}
</span>
<Folder className="size-8 text-muted-foreground" />
</div>
)}
{directories && directories?.length > 0 && (
<>
<Tree
data={directories}
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg"
onSelectChange={(item) => setFile(item?.id || null)}
folderIcon={Folder}
itemIcon={Workflow}
/>
<div className="w-full">
{file ? (
<ShowTraefikFile path={file} serverId={serverId} />
) : (
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
<span className="text-muted-foreground text-lg font-medium">
No file selected
</span>
<FileIcon className="size-8 text-muted-foreground" />
</div>
)}
</div>
</>
)}
</div>
</div>
</>
)}
</div>
</CardContent>
</div>
</Card>
</div>
);
};
@@ -1,129 +0,0 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
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 React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { ShowVolumes } from "../volumes/show-volumes";
import { ShowMariadbResources } from "./show-mariadb-resources";
const addDockerImage = z.object({
dockerImage: z.string().min(1, "Docker image is required"),
command: z.string(),
});
interface Props {
mariadbId: string;
}
type AddDockerImage = z.infer<typeof addDockerImage>;
export const ShowAdvancedMariadb = ({ mariadbId }: Props) => {
const { data, refetch } = api.mariadb.one.useQuery(
{
mariadbId,
},
{ enabled: !!mariadbId },
);
const { mutateAsync } = api.mariadb.update.useMutation();
const form = useForm<AddDockerImage>({
defaultValues: {
dockerImage: "",
command: "",
},
resolver: zodResolver(addDockerImage),
});
useEffect(() => {
if (data) {
form.reset({
dockerImage: data.dockerImage,
command: data.command || "",
});
}
}, [data, form, form.formState.isSubmitSuccessful, form.reset]);
const onSubmit = async (formData: AddDockerImage) => {
await mutateAsync({
mariadbId,
dockerImage: formData?.dockerImage,
command: formData?.command,
})
.then(async () => {
toast.success("Docker Image Updated");
await refetch();
})
.catch(() => {
toast.error("Error updating the resources");
});
};
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Advanced Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
>
<div className="grid w-full gap-4">
<FormField
control={form.control}
name="dockerImage"
render={({ field }) => (
<FormItem>
<FormLabel>Docker Image</FormLabel>
<FormControl>
<Input placeholder="mariadb:16" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>Command</FormLabel>
<FormControl>
<Input placeholder="Custom command" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button isLoading={form.formState.isSubmitting} type="submit">
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
<ShowVolumes mariadbId={mariadbId} />
<ShowMariadbResources mariadbId={mariadbId} />
</div>
</>
);
};
@@ -1,296 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
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 {
TooltipProvider,
TooltipTrigger,
TooltipContent,
Tooltip,
} from "@/components/ui/tooltip";
import { InfoIcon } from "lucide-react";
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addResourcesMariadb = z.object({
memoryReservation: z.number().nullable().optional(),
cpuLimit: z.number().nullable().optional(),
memoryLimit: z.number().nullable().optional(),
cpuReservation: z.number().nullable().optional(),
});
interface Props {
mariadbId: string;
}
type AddResourcesMariadb = z.infer<typeof addResourcesMariadb>;
export const ShowMariadbResources = ({ mariadbId }: Props) => {
const { data, refetch } = api.mariadb.one.useQuery(
{
mariadbId,
},
{ enabled: !!mariadbId },
);
const { mutateAsync, isLoading } = api.mariadb.update.useMutation();
const form = useForm<AddResourcesMariadb>({
defaultValues: {},
resolver: zodResolver(addResourcesMariadb),
});
useEffect(() => {
if (data) {
form.reset({
cpuLimit: data?.cpuLimit || undefined,
cpuReservation: data?.cpuReservation || undefined,
memoryLimit: data?.memoryLimit || undefined,
memoryReservation: data?.memoryReservation || undefined,
});
}
}, [data, form, form.formState.isSubmitSuccessful, form.reset]);
const onSubmit = async (formData: AddResourcesMariadb) => {
await mutateAsync({
mariadbId,
cpuLimit: formData.cpuLimit || null,
cpuReservation: formData.cpuReservation || null,
memoryLimit: formData.memoryLimit || null,
memoryReservation: formData.memoryReservation || null,
})
.then(async () => {
toast.success("Resources Updated");
await refetch();
})
.catch(() => {
toast.error("Error updating the resources");
});
};
return (
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Resources</CardTitle>
<CardDescription>
If you want to decrease or increase the resources to a specific.
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<AlertBlock type="info">
Please remember to click Redeploy after modify the resources to apply
the changes.
</AlertBlock>
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="grid w-full md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="268435456 (256MB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="memoryLimit"
render={({ field }) => {
return (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="1073741824 (1GB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="cpuLimit"
render={({ field }) => {
return (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>CPU Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="cpuReservation"
render={({ field }) => {
return (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>CPU Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="1000000000 (1 CPU)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
);
};
@@ -1,178 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { DatabaseBackup, Play } from "lucide-react";
import Link from "next/link";
import React from "react";
import { toast } from "sonner";
import { AddBackup } from "../../database/backups/add-backup";
import { DeleteBackup } from "../../database/backups/delete-backup";
import { UpdateBackup } from "../../database/backups/update-backup";
interface Props {
mariadbId: string;
}
export const ShowBackupMariadb = ({ mariadbId }: Props) => {
const { data } = api.destination.all.useQuery();
const { data: mariadb, refetch: refetchMariadb } = api.mariadb.one.useQuery(
{
mariadbId,
},
{
enabled: !!mariadbId,
},
);
const { mutateAsync: manualBackup, isLoading: isManualBackup } =
api.backup.manualBackupMariadb.useMutation();
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
<div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle>
<CardDescription>
Add backups to your database to save the data to a different
providers.
</CardDescription>
</div>
{mariadb && mariadb?.backups?.length > 0 && (
<AddBackup
databaseId={mariadbId}
databaseType="mariadb"
refetch={refetchMariadb}
/>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To create a backup it is required to set at least 1 provider.
Please, go to{" "}
<Link
href="/dashboard/settings/server"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
) : (
<div>
{mariadb?.backups.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No backups configured
</span>
<AddBackup
databaseId={mariadbId}
databaseType="mariadb"
refetch={refetchMariadb}
/>
</div>
) : (
<div className="flex flex-col pt-2">
<div className="flex flex-col gap-6">
{mariadb?.backups.map((backup) => (
<div key={backup.backupId}>
<div className="flex w-full flex-col md:flex-row md:items-center justify-between gap-4 md:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 flex-col gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Destination</span>
<span className="text-sm text-muted-foreground">
{backup.destination.name}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Database</span>
<span className="text-sm text-muted-foreground">
{backup.database}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Scheduled</span>
<span className="text-sm text-muted-foreground">
{backup.schedule}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Prefix Storage</span>
<span className="text-sm text-muted-foreground">
{backup.prefix}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Enabled</span>
<span className="text-sm text-muted-foreground">
{backup.enabled ? "Yes" : "No"}
</span>
</div>
</div>
<div className="flex flex-row gap-4">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
isLoading={isManualBackup}
onClick={async () => {
await manualBackup({
backupId: backup.backupId as string,
})
.then(async () => {
toast.success(
"Manual Backup Successful",
);
})
.catch(() => {
toast.error(
"Error creating the manual backup",
);
});
}}
>
<Play className="size-5 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent>Run Manual Backup</TooltipContent>
</Tooltip>
</TooltipProvider>
<UpdateBackup
backupId={backup.backupId}
refetch={refetchMariadb}
/>
<DeleteBackup
backupId={backup.backupId}
refetch={refetchMariadb}
/>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
);
};
@@ -1,158 +0,0 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Copy, TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteMariadbSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteMariadb = z.infer<typeof deleteMariadbSchema>;
interface Props {
mariadbId: string;
}
export const DeleteMariadb = ({ mariadbId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
const { data } = api.mariadb.one.useQuery(
{ mariadbId },
{ enabled: !!mariadbId },
);
const { push } = useRouter();
const form = useForm<DeleteMariadb>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteMariadbSchema),
});
const onSubmit = async (formData: DeleteMariadb) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ mariadbId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-mariadb"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<span>
To confirm, type{" "}
<Badge
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
variant="outline"
onClick={() => {
if (data?.name && data?.appName) {
navigator.clipboard.writeText(
`${data.name}/${data.appName}`,
);
toast.success("Copied to clipboard. Be careful!");
}
}}
>
{data?.name}/{data?.appName}&nbsp;
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
</Badge>{" "}
in the box below:
</span>
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-mariadb"
type="submit"
variant="destructive"
>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -1,73 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
mariadbId: string;
}
export const DeployMariadb = ({ mariadbId }: Props) => {
const { data, refetch } = api.mariadb.one.useQuery(
{
mariadbId,
},
{ enabled: !!mariadbId },
);
const { mutateAsync: deploy } = api.mariadb.deploy.useMutation();
const { mutateAsync: changeStatus } = api.mariadb.changeStatus.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button isLoading={data?.applicationStatus === "running"}>
Deploy
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will deploy the mariadb database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await changeStatus({
mariadbId,
applicationStatus: "running",
})
.then(async () => {
toast.success("Deploying Database....");
await refetch();
await deploy({
mariadbId,
}).catch(() => {
toast.error("Error deploying Database");
});
await refetch();
})
.catch((e) => {
toast.error(e.message || "Error deploying Database");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,69 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import { toast } from "sonner";
interface Props {
mariadbId: string;
appName: string;
}
export const ResetMariadb = ({ mariadbId, appName }: Props) => {
const { refetch } = api.mariadb.one.useQuery(
{
mariadbId,
},
{ enabled: !!mariadbId },
);
const { mutateAsync: reload, isLoading } = api.mariadb.reload.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will reload the service
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await reload({
mariadbId,
appName,
})
.then(() => {
toast.success("Service Reloaded");
})
.catch(() => {
toast.error("Error reloading the service");
});
await refetch();
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,26 +1,62 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { Terminal } from "lucide-react";
import React from "react";
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
import React, { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
import { StartMariadb } from "../start-mariadb";
import { DeployMariadb } from "./deploy-mariadb";
import { ResetMariadb } from "./reset-mariadb";
import { StopMariadb } from "./stop-mariadb";
interface Props {
mariadbId: string;
}
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
const { data } = api.mariadb.one.useQuery(
const { data, refetch } = api.mariadb.one.useQuery(
{
mariadbId,
},
{ enabled: !!mariadbId },
);
const { mutateAsync: reload, isLoading: isReloading } =
api.mariadb.reload.useMutation();
const { mutateAsync: start, isLoading: isStarting } =
api.mariadb.start.useMutation();
const { mutateAsync: stop, isLoading: isStopping } =
api.mariadb.stop.useMutation();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
api.mariadb.deployWithLogs.useSubscription(
{
mariadbId: mariadbId,
},
{
enabled: isDeploying,
onData(log) {
if (!isDrawerOpen) {
setIsDrawerOpen(true);
}
if (log === "Deployment completed successfully!") {
setIsDeploying(false);
}
const parsedLogs = parseLogs(log);
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
},
onError(error) {
console.error("Deployment logs error:", error);
setIsDeploying(false);
},
},
);
return (
<>
<div className="flex w-full flex-col gap-5 ">
@@ -29,12 +65,91 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<DeployMariadb mariadbId={mariadbId} />
<ResetMariadb mariadbId={mariadbId} appName={data?.appName || ""} />
<DialogAction
title="Deploy Mariadb"
description="Are you sure you want to deploy this mariadb?"
type="default"
onClick={async () => {
setIsDeploying(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
refetch();
}}
>
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
>
Deploy
</Button>
</DialogAction>
<DialogAction
title="Reload Mariadb"
description="Are you sure you want to reload this mariadb?"
type="default"
onClick={async () => {
await reload({
mariadbId: mariadbId,
appName: data?.appName || "",
})
.then(() => {
toast.success("Mariadb reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mariadb");
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
<StartMariadb mariadbId={mariadbId} />
<DialogAction
title="Start Mariadb"
description="Are you sure you want to start this mariadb?"
type="default"
onClick={async () => {
await start({
mariadbId: mariadbId,
})
.then(() => {
toast.success("Mariadb started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Mariadb");
});
}}
>
<Button variant="secondary" isLoading={isStarting}>
Start
<CheckCircle2 className="size-4" />
</Button>
</DialogAction>
) : (
<StopMariadb mariadbId={mariadbId} />
<DialogAction
title="Stop Mariadb"
description="Are you sure you want to stop this mariadb?"
onClick={async () => {
await stop({
mariadbId: mariadbId,
})
.then(() => {
toast.success("Mariadb stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Mariadb");
});
}}
>
<Button variant="destructive" isLoading={isStopping}>
Stop
<Ban className="size-4" />
</Button>
</DialogAction>
)}
<DockerTerminalModal
appName={data?.appName || ""}
@@ -47,6 +162,16 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</DockerTerminalModal>
</CardContent>
</Card>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>
</div>
</>
);
@@ -1,65 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Ban } from "lucide-react";
import { toast } from "sonner";
interface Props {
mariadbId: string;
}
export const StopMariadb = ({ mariadbId }: Props) => {
const { mutateAsync, isLoading } = api.mariadb.stop.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Stop
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure to stop the database?
</AlertDialogTitle>
<AlertDialogDescription>
This will stop the database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mariadbId,
})
.then(async () => {
await utils.mariadb.one.invalidate({
mariadbId,
});
toast.success("Application stopped successfully");
})
.catch(() => {
toast.error("Error stopping the Application");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -1,65 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
mariadbId: string;
}
export const StartMariadb = ({ mariadbId }: Props) => {
const { mutateAsync, isLoading } = api.mariadb.start.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Start
<CheckCircle2 className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to start the database?
</AlertDialogTitle>
<AlertDialogDescription>
This will start the database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mariadbId,
})
.then(async () => {
await utils.mariadb.one.invalidate({
mariadbId,
});
toast.success("Database started successfully");
})
.catch(() => {
toast.error("Error starting the Database");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } from "lucide-react";
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -89,8 +89,12 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
@@ -1,133 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { AlertTriangle, Package } from "lucide-react";
import React from "react";
import { AddVolumes } from "../../application/advanced/volumes/add-volumes";
import { DeleteVolume } from "../../application/advanced/volumes/delete-volume";
import { UpdateVolume } from "../../application/advanced/volumes/update-volume";
interface Props {
mariadbId: string;
}
export const ShowVolumes = ({ mariadbId }: Props) => {
const { data, refetch } = api.mariadb.one.useQuery(
{
mariadbId,
},
{ enabled: !!mariadbId },
);
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
<div>
<CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription>
If you want to persist data in this mariadb use the following config
to setup the volumes
</CardDescription>
</div>
{data && data?.mounts.length > 0 && (
<AddVolumes
serviceId={mariadbId}
refetch={refetch}
serviceType="mariadb"
>
Add Volume
</AddVolumes>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data?.mounts.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<Package className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No volumes/mounts configured
</span>
<AddVolumes
serviceId={mariadbId}
refetch={refetch}
serviceType="mariadb"
>
Add Volume
</AddVolumes>
</div>
) : (
<div className="flex flex-col pt-2 gap-4">
<AlertBlock type="info">
Please remember to click Redeploy after adding, editing, or
deleting a mount to apply the changes.
</AlertBlock>
<div className="flex flex-col gap-6">
{data?.mounts.map((mount) => (
<div key={mount.mountId}>
<div
key={mount.mountId}
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground">
{mount.type.toUpperCase()}
</span>
</div>
{mount.type === "volume" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Volume Name</span>
<span className="text-sm text-muted-foreground">
{mount.volumeName}
</span>
</div>
)}
{mount.type === "file" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground">
{mount.content}
</span>
</div>
)}
{mount.type === "bind" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Host Path</span>
<span className="text-sm text-muted-foreground">
{mount.hostPath}
</span>
</div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
</div>
<div className="flex flex-row gap-1">
<UpdateVolume
mountId={mount.mountId}
type={mount.type}
refetch={refetch}
serviceType="mariadb"
/>
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
};

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