Compare commits

...

291 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
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
Tam Nguyen
4a9d7225c9 chore: typo "accesed" changed to "accessed" for TS code 2025-01-08 13:44:59 +11: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
296 changed files with 61989 additions and 12010 deletions

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:20.9.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
- feat/add-sidebar
- build-arm64:
filters:
branches:
only:
- main
- canary
- feat/add-sidebar
- combine-manifests:
requires:
- build-amd64
- build-arm64
filters:
branches:
only:
- main
- canary
- feat/add-sidebar

83
.github/workflows/create-pr.yml vendored Normal file
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 }}

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
.github/workflows/dokploy.yml vendored Normal file
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
.github/workflows/monitoring.yml vendored Normal file
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
.gitignore vendored
View File

@@ -39,3 +39,6 @@ yarn-error.log*
# Misc
.DS_Store
*.pem
.db

View File

@@ -1,4 +1,4 @@
FROM node:20-slim AS base
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

View File

@@ -1,4 +1,4 @@
FROM node:20-slim AS base
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

41
Dockerfile.monitoring Normal file
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"]

View File

@@ -1,4 +1,4 @@
FROM node:20-slim AS base
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

View File

@@ -1,4 +1,4 @@
FROM node:20-slim AS base
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

View File

@@ -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",
);
});
});

View File

@@ -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",

View File

@@ -13,6 +13,7 @@ export default defineConfig({
NODE: "test",
},
},
plugins: [tsconfigPaths()],
resolve: {
alias: {
"@dokploy/server": path.resolve(

View File

@@ -144,38 +144,6 @@ export const ShowResources = ({ id, type }: 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}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="memoryLimit"
@@ -209,6 +177,37 @@ export const ShowResources = ({ id, type }: 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}

View File

@@ -45,8 +45,8 @@ export const ShowVolumes = ({ id, type }: Props) => {
<div>
<CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription>
If you want to persist data in this postgres database 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>
@@ -100,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
{mount.type === "file" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground">
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
{mount.content}
</span>
</div>
@@ -113,12 +113,21 @@ export const ShowVolumes = ({ id, type }: 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

View File

@@ -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>

View File

@@ -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>

View File

@@ -7,7 +7,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
import Link from "next/link";
@@ -74,60 +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>
<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}
<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}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<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>
);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>
);

View File

@@ -20,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 "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -39,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: {
@@ -62,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", {
@@ -95,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">
@@ -119,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!");
}
}}
@@ -142,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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -7,7 +7,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
import Link from "next/link";
@@ -74,59 +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>
<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}
<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}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<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>
);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -75,14 +75,14 @@ export const ShowBackups = ({ id, type }: Props) => {
{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>

View File

@@ -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);

View File

@@ -80,7 +80,7 @@ export const ShowContainers = ({ serverId }: Props) => {
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto">
<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">

View File

@@ -35,7 +35,7 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto">
<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">

View File

@@ -1,314 +0,0 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { api } from "@/utils/api";
import React, { useEffect, useState } from "react";
import { DockerBlockChart } from "./docker-block-chart";
import { DockerCpuChart } from "./docker-cpu-chart";
import { DockerDiskChart } from "./docker-disk-chart";
import { DockerMemoryChart } from "./docker-memory-chart";
import { DockerNetworkChart } from "./docker-network-chart";
const defaultData = {
cpu: {
value: 0,
time: "",
},
memory: {
value: {
used: 0,
free: 0,
usedPercentage: 0,
total: 0,
},
time: "",
},
block: {
value: {
readMb: 0,
writeMb: 0,
},
time: "",
},
network: {
value: {
inputMb: 0,
outputMb: 0,
},
time: "",
},
disk: {
value: { diskTotal: 0, diskUsage: 0, diskUsedPercentage: 0, diskFree: 0 },
time: "",
},
};
interface Props {
appName: string;
appType?: "application" | "stack" | "docker-compose";
}
export interface DockerStats {
cpu: {
value: number;
time: string;
};
memory: {
value: {
used: number;
free: number;
usedPercentage: number;
total: number;
};
time: string;
};
block: {
value: {
readMb: number;
writeMb: number;
};
time: string;
};
network: {
value: {
inputMb: number;
outputMb: number;
};
time: string;
};
disk: {
value: {
diskTotal: number;
diskUsage: number;
diskUsedPercentage: number;
diskFree: number;
};
time: string;
};
}
export type DockerStatsJSON = {
cpu: DockerStats["cpu"][];
memory: DockerStats["memory"][];
block: DockerStats["block"][];
network: DockerStats["network"][];
disk: DockerStats["disk"][];
};
export const DockerMonitoring = ({
appName,
appType = "application",
}: Props) => {
const { data } = api.application.readAppMonitoring.useQuery(
{ appName },
{
refetchOnWindowFocus: false,
},
);
const [acummulativeData, setAcummulativeData] = useState<DockerStatsJSON>({
cpu: [],
memory: [],
block: [],
network: [],
disk: [],
});
const [currentData, setCurrentData] = useState<DockerStats>(defaultData);
useEffect(() => {
setCurrentData(defaultData);
setAcummulativeData({
cpu: [],
memory: [],
block: [],
network: [],
disk: [],
});
}, [appName]);
useEffect(() => {
if (!data) return;
setCurrentData({
cpu: data.cpu[data.cpu.length - 1] ?? currentData.cpu,
memory: data.memory[data.memory.length - 1] ?? currentData.memory,
block: data.block[data.block.length - 1] ?? currentData.block,
network: data.network[data.network.length - 1] ?? currentData.network,
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
});
setAcummulativeData({
block: data?.block || [],
cpu: data?.cpu || [],
disk: data?.disk || [],
memory: data?.memory || [],
network: data?.network || [],
});
}, [data]);
useEffect(() => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-docker-stats-monitoring?appName=${appName}&appType=${appType}`;
const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => {
const value = JSON.parse(e.data);
if (!value) return;
const data = {
cpu: value.data.cpu ?? currentData.cpu,
memory: value.data.memory ?? currentData.memory,
block: value.data.block ?? currentData.block,
disk: value.data.disk ?? currentData.disk,
network: value.data.network ?? currentData.network,
};
setCurrentData(data);
setAcummulativeData((prevData) => ({
cpu: [...prevData.cpu, data.cpu],
memory: [...prevData.memory, data.memory],
block: [...prevData.block, data.block],
network: [...prevData.network, data.network],
disk: [...prevData.disk, data.disk],
}));
};
ws.onclose = (e) => {
console.log(e.reason);
};
return () => ws.close();
}, [appName]);
return (
<div>
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
<header className="flex items-center justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">
Monitoring
</h1>
<p className="text-sm text-muted-foreground">
Watch the usage of your server in the current app
</p>
</div>
</header>
<div className="grid gap-6 md:grid-cols-2">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
Used: {currentData.cpu.value.toFixed(2)}%
</span>
<Progress
value={currentData.cpu.value}
className="w-[100%]"
/>
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Memory Usage
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
</span>
<Progress
value={currentData.memory.value.usedPercentage}
className="w-[100%]"
/>
<DockerMemoryChart
acummulativeData={acummulativeData.memory}
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
/>
</div>
</CardContent>
</Card>
{appName === "dokploy" && (
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Disk Space
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
</span>
<Progress
value={currentData.disk.value.diskUsedPercentage}
className="w-[100%]"
/>
<DockerDiskChart
acummulativeData={acummulativeData.disk}
diskTotal={currentData.disk.value.diskTotal}
/>
</div>
</CardContent>
</Card>
)}
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Block I/O</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Read: ${currentData.block.value.readMb.toFixed(
2,
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
3,
)} MB`}
</span>
<DockerBlockChart acummulativeData={acummulativeData.block} />
</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Network I/O
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`In MB: ${currentData.network.value.inputMb.toFixed(
2,
)} MB / Out MB: ${currentData.network.value.outputMb.toFixed(
2,
)} MB`}
</span>
<DockerNetworkChart
acummulativeData={acummulativeData.network}
/>
</div>
</CardContent>
</Card>
</div>
</div>
</Card>
</div>
);
};

View File

@@ -8,7 +8,7 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["block"];
@@ -90,9 +90,11 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length && payload[0]) {
return (
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
<p>{`Read ${payload[0].payload.readMb.toFixed(2)} MB`}</p>
<p>{`Write: ${payload[0].payload.writeMb.toFixed(3)} MB`}</p>
{payload[0].payload.time && (
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
)}
<p>{`Read ${payload[0].payload.readMb} `}</p>
<p>{`Write: ${payload[0].payload.writeMb} `}</p>
</div>
);
}

View File

@@ -8,7 +8,7 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["cpu"];
@@ -19,7 +19,7 @@ export const DockerCpuChart = ({ acummulativeData }: Props) => {
return {
name: `Point ${index + 1}`,
time: item.time,
usage: item.value.toFixed(2),
usage: item.value.toString().split("%")[0],
};
});
return (
@@ -75,7 +75,9 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length && payload[0]) {
return (
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
{payload[0].payload.time && (
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
)}
<p>{`CPU Usage: ${payload[0].payload.usage}%`}</p>
</div>
);

View File

@@ -8,7 +8,7 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["disk"];

View File

@@ -8,8 +8,8 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
import { convertMemoryToBytes } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["memory"];
memoryLimitGB: number;
@@ -23,7 +23,8 @@ export const DockerMemoryChart = ({
return {
time: item.time,
name: `Point ${index + 1}`,
usage: (item.value.used / 1024 ** 3).toFixed(2),
// @ts-ignore
usage: (convertMemoryToBytes(item.value.used) / 1024 ** 3).toFixed(2),
};
});
return (
@@ -75,10 +76,13 @@ interface CustomTooltipProps {
}
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length && payload[0]) {
if (active && payload && payload.length && payload[0] && payload[0].payload) {
return (
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
{payload[0].payload.time && (
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
)}
<p>{`Memory usage: ${payload[0].payload.usage} GB`}</p>
</div>
);

View File

@@ -8,8 +8,7 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
1;
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["network"];
}
@@ -19,8 +18,8 @@ export const DockerNetworkChart = ({ acummulativeData }: Props) => {
return {
time: item.time,
name: `Point ${index + 1}`,
inMB: item.value.inputMb.toFixed(2),
outMB: item.value.outputMb.toFixed(2),
inMB: item.value.inputMb,
outMB: item.value.outputMb,
};
});
return (
@@ -86,9 +85,11 @@ const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length && payload[0]) {
return (
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
<p>{`In MB Usage: ${payload[0].payload.inMB} MB`}</p>
<p>{`Out MB Usage: ${payload[0].payload.outMB} MB`}</p>
{payload[0].payload.time && (
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
)}
<p>{`In Usage: ${payload[0].payload.inMB} `}</p>
<p>{`Out Usage: ${payload[0].payload.outMB} `}</p>
</div>
);
}

View File

@@ -0,0 +1,126 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { ContainerFreeMonitoring } from "./show-free-container-monitoring";
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
}
export const ComposeFreeMonitoring = ({
appName,
appType = "stack",
serverId,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName: appName,
appType,
serverId,
},
{
enabled: !!appName,
},
);
const [containerAppName, setContainerAppName] = useState<
string | undefined
>();
const [containerId, setContainerId] = useState<string | undefined>();
const { mutateAsync: restart, isLoading: isRestarting } =
api.docker.restartContainer.useMutation();
useEffect(() => {
if (data && data?.length > 0) {
setContainerAppName(data[0]?.name);
setContainerId(data[0]?.containerId);
}
}, [data]);
return (
<>
<CardHeader>
<CardTitle className="text-xl">Monitoring</CardTitle>
<CardDescription>Watch the usage of your compose</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Label>Select a container to watch the monitoring</Label>
<div className="flex flex-row gap-4">
<Select
onValueChange={(value) => {
setContainerAppName(value);
setContainerId(
data?.find((container) => container.name === value)
?.containerId,
);
}}
value={containerAppName}
>
<SelectTrigger>
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((container) => (
<SelectItem
key={container.containerId}
value={container.name}
>
{container.name} ({container.containerId}) {container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<Button
isLoading={isRestarting}
onClick={async () => {
if (!containerId) return;
toast.success(`Restarting container ${containerAppName}`);
await restart({ containerId }).then(() => {
toast.success("Container restarted");
});
}}
>
Restart
</Button>
</div>
<ContainerFreeMonitoring
appName={containerAppName || ""}
appType={appType}
/>
</CardContent>
</>
);
};

View File

@@ -0,0 +1,316 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { api } from "@/utils/api";
import React, { useEffect, useState } from "react";
import { DockerBlockChart } from "./docker-block-chart";
import { DockerCpuChart } from "./docker-cpu-chart";
import { DockerDiskChart } from "./docker-disk-chart";
import { DockerMemoryChart } from "./docker-memory-chart";
import { DockerNetworkChart } from "./docker-network-chart";
const defaultData = {
cpu: {
value: 0,
time: "",
},
memory: {
value: {
used: 0,
total: 0,
},
time: "",
},
block: {
value: {
readMb: 0,
writeMb: 0,
},
time: "",
},
network: {
value: {
inputMb: 0,
outputMb: 0,
},
time: "",
},
disk: {
value: { diskTotal: 0, diskUsage: 0, diskUsedPercentage: 0, diskFree: 0 },
time: "",
},
};
interface Props {
appName: string;
appType?: "application" | "stack" | "docker-compose";
}
export interface DockerStats {
cpu: {
value: number;
time: string;
};
memory: {
value: {
used: number;
total: number;
};
time: string;
};
block: {
value: {
readMb: number;
writeMb: number;
};
time: string;
};
network: {
value: {
inputMb: number;
outputMb: number;
};
time: string;
};
disk: {
value: {
diskTotal: number;
diskUsage: number;
diskUsedPercentage: number;
diskFree: number;
};
time: string;
};
}
export type DockerStatsJSON = {
cpu: DockerStats["cpu"][];
memory: DockerStats["memory"][];
block: DockerStats["block"][];
network: DockerStats["network"][];
disk: DockerStats["disk"][];
};
export const convertMemoryToBytes = (
memoryString: string | undefined,
): number => {
if (!memoryString || typeof memoryString !== "string") {
return 0;
}
const value = Number.parseFloat(memoryString) || 0;
const unit = memoryString.replace(/[0-9.]/g, "").trim();
switch (unit) {
case "KiB":
return value * 1024;
case "MiB":
return value * 1024 * 1024;
case "GiB":
return value * 1024 * 1024 * 1024;
case "TiB":
return value * 1024 * 1024 * 1024 * 1024;
default:
return value;
}
};
export const ContainerFreeMonitoring = ({
appName,
appType = "application",
}: Props) => {
const { data } = api.application.readAppMonitoring.useQuery(
{ appName },
{
refetchOnWindowFocus: false,
},
);
const [acummulativeData, setAcummulativeData] = useState<DockerStatsJSON>({
cpu: [],
memory: [],
block: [],
network: [],
disk: [],
});
const [currentData, setCurrentData] = useState<DockerStats>(defaultData);
useEffect(() => {
setCurrentData(defaultData);
setAcummulativeData({
cpu: [],
memory: [],
block: [],
network: [],
disk: [],
});
}, [appName]);
useEffect(() => {
if (!data) return;
setCurrentData({
cpu: data.cpu[data.cpu.length - 1] ?? currentData.cpu,
memory: data.memory[data.memory.length - 1] ?? currentData.memory,
block: data.block[data.block.length - 1] ?? currentData.block,
network: data.network[data.network.length - 1] ?? currentData.network,
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
});
setAcummulativeData({
block: data?.block || [],
cpu: data?.cpu || [],
disk: data?.disk || [],
memory: data?.memory || [],
network: data?.network || [],
});
}, [data]);
useEffect(() => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-docker-stats-monitoring?appName=${appName}&appType=${appType}`;
const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => {
const value = JSON.parse(e.data);
if (!value) return;
const data = {
cpu: value.data.cpu ?? currentData.cpu,
memory: value.data.memory ?? currentData.memory,
block: value.data.block ?? currentData.block,
disk: value.data.disk ?? currentData.disk,
network: value.data.network ?? currentData.network,
};
setCurrentData(data);
setAcummulativeData((prevData) => ({
cpu: [...prevData.cpu, data.cpu],
memory: [...prevData.memory, data.memory],
block: [...prevData.block, data.block],
network: [...prevData.network, data.network],
disk: [...prevData.disk, data.disk],
}));
};
ws.onclose = (e) => {
console.log(e.reason);
};
return () => ws.close();
}, [appName]);
return (
<div className="rounded-xl bg-background shadow-md flex flex-col gap-4">
<header className="flex items-center justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
<p className="text-sm text-muted-foreground">
Watch the usage of your server in the current app
</p>
</div>
</header>
<div className="grid gap-6 lg:grid-cols-2">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
Used: {currentData.cpu.value}%
</span>
<Progress value={currentData.cpu.value} className="w-[100%]" />
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Memory Usage</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Used: ${currentData.memory.value.used} / Limit: ${currentData.memory.value.total} `}
</span>
<Progress
value={
// @ts-ignore
(convertMemoryToBytes(currentData.memory.value.used) /
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total)) *
100
}
className="w-[100%]"
/>
<DockerMemoryChart
acummulativeData={acummulativeData.memory}
memoryLimitGB={
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total) /
1024 ** 3
}
/>
</div>
</CardContent>
</Card>
{appName === "dokploy" && (
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Disk Space</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
</span>
<Progress
value={currentData.disk.value.diskUsedPercentage}
className="w-[100%]"
/>
<DockerDiskChart
acummulativeData={acummulativeData.disk}
diskTotal={currentData.disk.value.diskTotal}
/>
</div>
</CardContent>
</Card>
)}
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Block I/O</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
</span>
<DockerBlockChart acummulativeData={acummulativeData.block} />
</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Network I/O</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
</span>
<DockerNetworkChart acummulativeData={acummulativeData.network} />
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@@ -0,0 +1,189 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface ContainerMetric {
timestamp: string;
BlockIO: {
read: number;
write: number;
readUnit: string;
writeUnit: string;
};
}
interface Props {
data: ContainerMetric[];
}
interface FormattedMetric {
timestamp: string;
read: number;
write: number;
readUnit: string;
writeUnit: string;
}
const chartConfig = {
read: {
label: "Read",
color: "hsl(217, 91%, 60%)", // Azul brillante
},
write: {
label: "Write",
color: "hsl(142, 71%, 45%)", // Verde brillante
},
} satisfies ChartConfig;
export const ContainerBlockChart = ({ data }: Props) => {
const formattedData = data.map((metric) => ({
timestamp: metric.timestamp,
read: metric.BlockIO.read,
write: metric.BlockIO.write,
readUnit: metric.BlockIO.readUnit,
writeUnit: metric.BlockIO.writeUnit,
}));
const latestData = formattedData[formattedData.length - 1] || {
timestamp: "",
read: 0,
write: 0,
readUnit: "B",
writeUnit: "B",
};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Block I/O</CardTitle>
<CardDescription>
Read: {latestData.read}
{latestData.readUnit} / Write: {latestData.write}
{latestData.writeUnit}
</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={formattedData}>
<defs>
<linearGradient id="fillRead" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(217, 91%, 60%)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="hsl(217, 91%, 60%)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillWrite" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(142, 71%, 45%)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="hsl(142, 71%, 45%)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Read
</span>
<span className="font-bold">
{data.read}
{data.readUnit}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Write
</span>
<span className="font-bold">
{data.write}
{data.writeUnit}
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="Write"
dataKey="write"
type="monotone"
fill="url(#fillWrite)"
stroke="hsl(142, 71%, 45%)"
strokeWidth={2}
fillOpacity={0.3}
/>
<Area
name="Read"
dataKey="read"
type="monotone"
fill="url(#fillRead)"
stroke="hsl(217, 91%, 60%)"
strokeWidth={2}
fillOpacity={0.3}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,128 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface ContainerMetric {
timestamp: string;
CPU: number;
}
interface Props {
data: ContainerMetric[];
}
const chartConfig = {
cpu: {
label: "CPU",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export const ContainerCPUChart = ({ data }: Props) => {
const formattedData = data.map((metric) => ({
timestamp: metric.timestamp,
cpu: metric.CPU,
}));
const latestData = formattedData[formattedData.length - 1] || {
timestamp: "",
cpu: 0,
};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>CPU</CardTitle>
<CardDescription>CPU Usage: {latestData.cpu}%</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={formattedData}>
<defs>
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
CPU
</span>
<span className="font-bold">{data.cpu}%</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="CPU"
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="hsl(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,149 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface ContainerMetric {
timestamp: string;
Memory: {
percentage: number;
used: number;
total: number;
usedUnit: string;
totalUnit: string;
};
}
interface Props {
data: ContainerMetric[];
}
const chartConfig = {
memory: {
label: "Memory",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
const formatMemoryValue = (value: number) => {
return value.toLocaleString("en-US", {
minimumFractionDigits: 1,
maximumFractionDigits: 2,
});
};
export const ContainerMemoryChart = ({ data }: Props) => {
const formattedData = data.map((metric) => ({
timestamp: metric.timestamp,
memory: metric.Memory.percentage,
usage: `${formatMemoryValue(metric.Memory.used)}${metric.Memory.usedUnit} / ${formatMemoryValue(metric.Memory.total)}${metric.Memory.totalUnit}`,
}));
const latestData = formattedData[formattedData.length - 1] || {
timestamp: "",
memory: 0,
usage: "0 / 0 B",
};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Memory</CardTitle>
<CardDescription>Memory Usage: {latestData.usage}</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={formattedData}>
<defs>
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Memory
</span>
<span className="font-bold">{data.memory}%</span>
</div>
<div className="flex flex-col col-span-2">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Usage
</span>
<span className="font-bold">{data.usage}</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="Memory"
dataKey="memory"
type="monotone"
fill="url(#fillMemory)"
stroke="hsl(var(--chart-2))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,186 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface ContainerMetric {
timestamp: string;
Network: {
input: number;
output: number;
inputUnit: string;
outputUnit: string;
};
}
interface Props {
data: ContainerMetric[];
}
interface FormattedMetric {
timestamp: string;
input: number;
output: number;
inputUnit: string;
outputUnit: string;
}
const chartConfig = {
input: {
label: "Input",
color: "hsl(var(--chart-3))",
},
output: {
label: "Output",
color: "hsl(var(--chart-4))",
},
} satisfies ChartConfig;
export const ContainerNetworkChart = ({ data }: Props) => {
const formattedData: FormattedMetric[] = data.map((metric) => ({
timestamp: metric.timestamp,
input: metric.Network.input,
output: metric.Network.output,
inputUnit: metric.Network.inputUnit,
outputUnit: metric.Network.outputUnit,
}));
const latestData = formattedData[formattedData.length - 1] || {
input: 0,
output: 0,
inputUnit: "B",
outputUnit: "B",
};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Network I/O</CardTitle>
<CardDescription>
Input: {latestData.input}
{latestData.inputUnit} / Output: {latestData.output}
{latestData.outputUnit}
</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={formattedData}>
<defs>
<linearGradient id="fillInput" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillOutput" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Input
</span>
<span className="font-bold">
{data.input}
{data.inputUnit}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Output
</span>
<span className="font-bold">
{data.output}
{data.outputUnit}
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="Input"
dataKey="input"
type="monotone"
fill="url(#fillInput)"
stroke="hsl(var(--chart-3))"
strokeWidth={2}
/>
<Area
name="Output"
dataKey="output"
type="monotone"
fill="url(#fillOutput)"
stroke="hsl(var(--chart-4))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
};

View File

@@ -20,18 +20,22 @@ import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { DockerMonitoring } from "../../monitoring/docker/show";
import { ContainerPaidMonitoring } from "./show-paid-container-monitoring";
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
baseUrl: string;
token: string;
}
export const ShowMonitoringCompose = ({
export const ComposePaidMonitoring = ({
appName,
appType = "stack",
serverId,
baseUrl,
token,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
@@ -44,9 +48,9 @@ export const ShowMonitoringCompose = ({
},
);
const [containerAppName, setContainerAppName] = useState<
string | undefined
>();
const [containerAppName, setContainerAppName] = useState<string | undefined>(
"",
);
const [containerId, setContainerId] = useState<string | undefined>();
@@ -62,7 +66,7 @@ export const ShowMonitoringCompose = ({
return (
<div>
<Card className="bg-background">
<Card className="bg-background border-0">
<CardHeader>
<CardTitle className="text-xl">Monitoring</CardTitle>
<CardDescription>Watch the usage of your compose</CardDescription>
@@ -118,10 +122,13 @@ export const ShowMonitoringCompose = ({
Restart
</Button>
</div>
<DockerMonitoring
appName={containerAppName || ""}
appType={appType}
/>
<div className="flex flex-col gap-4">
<ContainerPaidMonitoring
appName={containerAppName || ""}
baseUrl={baseUrl}
token={token}
/>
</div>
</CardContent>
</Card>
</div>

View File

@@ -0,0 +1,258 @@
import { Card } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Cpu, HardDrive, Loader2, MemoryStick, Network } from "lucide-react";
import { useEffect, useState } from "react";
import { ContainerBlockChart } from "./container-block-chart";
import { ContainerCPUChart } from "./container-cpu-chart";
import { ContainerMemoryChart } from "./container-memory-chart";
import { ContainerNetworkChart } from "./container-network-chart";
const REFRESH_INTERVALS = {
"5000": "5 Seconds",
"10000": "10 Seconds",
"20000": "20 Seconds",
"30000": "30 Seconds",
} as const;
const DATA_POINTS_OPTIONS = {
"50": "50 points",
"200": "200 points",
"500": "500 points",
"800": "800 points",
"1200": "1200 points",
"1600": "1600 points",
"2000": "2000 points",
all: "All points",
} as const;
interface ContainerMetric {
timestamp: string;
CPU: number;
Memory: {
percentage: number;
used: number;
total: number;
unit: string;
usedUnit: string;
totalUnit: string;
};
Network: {
input: number;
output: number;
inputUnit: string;
outputUnit: string;
};
BlockIO: {
read: number;
write: number;
readUnit: string;
writeUnit: string;
};
Container: string;
ID: string;
Name: string;
}
interface Props {
appName: string;
baseUrl: string;
token: string;
}
export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
const [historicalData, setHistoricalData] = useState<ContainerMetric[]>([]);
const [metrics, setMetrics] = useState<ContainerMetric>(
{} as ContainerMetric,
);
const [dataPoints, setDataPoints] =
useState<keyof typeof DATA_POINTS_OPTIONS>("50");
const [refreshInterval, setRefreshInterval] = useState<string>("5000");
const {
data,
isLoading,
error: queryError,
} = api.admin.getContainerMetrics.useQuery(
{
url: baseUrl,
token,
dataPoints,
appName,
},
{
refetchInterval:
dataPoints === "all" ? undefined : Number.parseInt(refreshInterval),
enabled: !!appName,
},
);
useEffect(() => {
if (!data) return;
// @ts-ignore
setHistoricalData(data);
// @ts-ignore
setMetrics(data[data.length - 1]);
}, [data]);
if (isLoading) {
return (
<div className="flex h-[400px] w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (queryError) {
return (
<div className="mt-5 flex min-h-[55vh] w-full items-center justify-center p-4">
<div className="max-w-xl text-center">
<p className="mb-2 text-base font-medium leading-none text-muted-foreground">
Error fetching metrics for{" "}
<strong className="text-primary">{appName}</strong>
</p>
<p className="whitespace-pre-line text-sm text-destructive">
{queryError instanceof Error
? queryError.message
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
</p>
<p className=" text-sm text-muted-foreground">URL: {baseUrl}</p>
</div>
</div>
);
}
return (
<>
<div className="flex items-center justify-between flex-wrap gap-2">
<h2 className="text-2xl font-bold tracking-tight">
Container Monitoring
</h2>
<div className="flex items-center gap-4 flex-wrap">
<div>
<span className="text-sm text-muted-foreground">Data points:</span>
<Select
value={dataPoints}
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
setDataPoints(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select points" />
</SelectTrigger>
<SelectContent>
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<span className="text-sm text-muted-foreground">
Refresh interval:
</span>
<Select
value={refreshInterval}
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
setRefreshInterval(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<Card className="p-6 bg-transparent">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">CPU Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">{metrics.CPU}%</p>
</Card>
<Card className="p-6 bg-transparent">
<div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Memory Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{metrics?.Memory?.percentage}%
</p>
<p className="mt-1 text-sm text-muted-foreground">
{metrics?.Memory?.used} {metrics?.Memory?.unit} /{" "}
{metrics?.Memory?.total} {metrics?.Memory?.unit}
</p>
</Card>
<Card className="p-6 bg-transparent">
<div className="flex items-center gap-2">
<Network className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Network I/O</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{metrics?.Network?.input} {metrics?.Network?.inputUnit} /{" "}
{metrics?.Network?.output} {metrics?.Network?.outputUnit}
</p>
</Card>
<Card className="p-6 bg-transparent">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Block I/O</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{metrics?.BlockIO?.read} {metrics?.BlockIO?.readUnit} /{" "}
{metrics?.BlockIO?.write} {metrics?.BlockIO?.writeUnit}
</p>
</Card>
</div>
{/* Container Information */}
<Card className="p-6 bg-transparent">
<h3 className="text-lg font-medium mb-4">Container Information</h3>
<div className="grid gap-4 md:grid-cols-2">
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Container ID
</h4>
<p className="mt-1">{metrics.ID}</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">Name</h4>
<p className="mt-1 truncate">{metrics.Name}</p>
</div>
</div>
</Card>
{/* Charts Grid */}
<div className="grid gap-4 grid-cols-1 md:grid-cols-1 xl:grid-cols-2">
<ContainerCPUChart data={historicalData} />
<ContainerMemoryChart data={historicalData} />
<ContainerBlockChart data={historicalData} />
<ContainerNetworkChart data={historicalData} />
</div>
</>
);
};

View File

@@ -0,0 +1,115 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface CPUChartProps {
data: any[];
}
const chartConfig = {
cpu: {
label: "CPU",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export function CPUChart({ data }: CPUChartProps) {
const latestData = data[data.length - 1] || {};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>CPU</CardTitle>
<CardDescription>CPU Usage: {latestData.cpu}%</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={data}>
<defs>
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
CPU
</span>
<span className="font-bold">{data.cpu}%</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="CPU"
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="hsl(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,120 @@
import { HardDrive } from "lucide-react";
import {
Label,
PolarGrid,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from "recharts";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
interface RadialChartProps {
data: any;
}
export function DiskChart({ data }: RadialChartProps) {
const diskUsed = Number.parseFloat(data.diskUsed || 0);
const totalDiskGB = Number.parseFloat(data.totalDisk || 0);
const usedDiskGB = (totalDiskGB * diskUsed) / 100;
const chartData = [
{
disk: 25,
fill: "hsl(var(--chart-2))",
},
];
const chartConfig = {
disk: {
label: "Disk",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
const endAngle = (diskUsed * 360) / 100;
return (
<Card className="flex flex-col bg-transparent">
<CardHeader className="items-center border-b pb-5">
<CardTitle>Disk</CardTitle>
<CardDescription>Storage Space</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<RadialBarChart
data={chartData}
startAngle={0}
endAngle={endAngle}
innerRadius={80}
outerRadius={110}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[86, 74]}
/>
<RadialBar
dataKey="disk"
background
cornerRadius={10}
fill="hsl(var(--chart-2))"
/>
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-4xl font-bold"
>
{diskUsed.toFixed(1)}%
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground text-sm"
>
Used
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
<HardDrive className="h-4 w-4" /> {usedDiskGB.toFixed(1)} GB used
</div>
<div className="leading-none text-muted-foreground">
Of {totalDiskGB.toFixed(1)} GB total
</div>
</CardFooter>
</Card>
);
}

View File

@@ -0,0 +1,128 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface MemoryChartProps {
data: any[];
}
const chartConfig = {
Memory: {
label: "Memory",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
export function MemoryChart({ data }: MemoryChartProps) {
const latestData = data[data.length - 1] || {};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Memory</CardTitle>
<CardDescription>
Memory Usage: {latestData.memUsedGB} GB of {latestData.memTotal} GB (
{latestData.memUsed}%)
</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={data}>
<defs>
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis
yAxisId="left"
orientation="left"
tickFormatter={(value) => `${value}%`}
domain={[0, 100]}
/>
<YAxis
yAxisId="right"
orientation="right"
tickFormatter={(value) => `${value.toFixed(1)} GB`}
domain={[
0,
Math.ceil(Number.parseFloat(latestData.memTotal || "0")),
]}
/>
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Memory
</span>
<span className="font-bold">
{data.memUsed}% ({data.memUsedGB} GB)
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
yAxisId="left"
dataKey="memUsed"
type="monotone"
fill="url(#fillMemory)"
stroke="hsl(var(--chart-2))"
strokeWidth={2}
name="Memory"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,145 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface NetworkChartProps {
data: any[];
}
const chartConfig = {
networkIn: {
label: "Network In",
color: "hsl(var(--chart-3))",
},
networkOut: {
label: "Network Out",
color: "hsl(var(--chart-4))",
},
} satisfies ChartConfig;
export function NetworkChart({ data }: NetworkChartProps) {
const latestData = data[data.length - 1] || {};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Network</CardTitle>
<CardDescription>
Network Traffic: {latestData.networkOut} KB/s {" "}
{latestData.networkIn} KB/s
</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={data}>
<defs>
<linearGradient id="fillNetworkIn" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillNetworkOut" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis tickFormatter={(value) => `${value} KB/s`} />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Network
</span>
<span className="font-bold">
{data.networkOut} KB/s
<br /> {data.networkIn} KB/s
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="Network In"
dataKey="networkIn"
type="monotone"
fill="url(#fillNetworkIn)"
stroke="hsl(var(--chart-3))"
strokeWidth={2}
/>
<Area
name="Network Out"
dataKey="networkOut"
type="monotone"
fill="url(#fillNetworkOut)"
stroke="hsl(var(--chart-4))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,276 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react";
import type React from "react";
import { useEffect, useState } from "react";
import { CPUChart } from "./cpu-chart";
import { DiskChart } from "./disk-chart";
import { MemoryChart } from "./memory-chart";
import { NetworkChart } from "./network-chart";
const REFRESH_INTERVALS = {
"5000": "5 Seconds",
"10000": "10 Seconds",
"20000": "20 Seconds",
"30000": "30 Seconds",
} as const;
const DATA_POINTS_OPTIONS = {
"50": "50 points",
"200": "200 points",
"500": "500 points",
"800": "800 points",
"1200": "1200 points",
"1600": "1600 points",
"2000": "2000 points",
all: "All points",
} as const;
interface SystemMetrics {
cpu: string;
cpuModel: string;
cpuCores: number;
cpuPhysicalCores: number;
cpuSpeed: number;
os: string;
distro: string;
kernel: string;
arch: string;
memUsed: string;
memUsedGB: string;
memTotal: string;
uptime: number;
diskUsed: string;
totalDisk: string;
networkIn: string;
networkOut: string;
timestamp: string;
}
interface Props {
BASE_URL?: string;
token?: string;
}
export const ShowPaidMonitoring = ({
BASE_URL = process.env.NEXT_PUBLIC_METRICS_URL ||
"http://localhost:3001/metrics",
token = process.env.NEXT_PUBLIC_METRICS_TOKEN || "my-token",
}: Props) => {
const [historicalData, setHistoricalData] = useState<SystemMetrics[]>([]);
const [metrics, setMetrics] = useState<SystemMetrics>({} as SystemMetrics);
const [dataPoints, setDataPoints] =
useState<keyof typeof DATA_POINTS_OPTIONS>("50");
const [refreshInterval, setRefreshInterval] = useState<string>("5000");
const {
data,
isLoading,
error: queryError,
} = api.admin.getServerMetrics.useQuery(
{
url: BASE_URL,
token,
dataPoints,
},
{
refetchInterval:
dataPoints === "all" ? undefined : Number.parseInt(refreshInterval),
enabled: true,
},
);
useEffect(() => {
if (!data) return;
const formattedData = data.map((metric: SystemMetrics) => ({
timestamp: metric.timestamp,
cpu: Number.parseFloat(metric.cpu),
cpuModel: metric.cpuModel,
cpuCores: metric.cpuCores,
cpuPhysicalCores: metric.cpuPhysicalCores,
cpuSpeed: metric.cpuSpeed,
os: metric.os,
distro: metric.distro,
kernel: metric.kernel,
arch: metric.arch,
memUsed: Number.parseFloat(metric.memUsed),
memUsedGB: Number.parseFloat(metric.memUsedGB),
memTotal: Number.parseFloat(metric.memTotal),
networkIn: Number.parseFloat(metric.networkIn),
networkOut: Number.parseFloat(metric.networkOut),
diskUsed: Number.parseFloat(metric.diskUsed),
totalDisk: Number.parseFloat(metric.totalDisk),
uptime: metric.uptime,
}));
// @ts-ignore
setHistoricalData(formattedData);
// @ts-ignore
setMetrics(formattedData[formattedData.length - 1] || {});
}, [data]);
const formatUptime = (seconds: number): string => {
const days = Math.floor(seconds / (24 * 60 * 60));
const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60));
const minutes = Math.floor((seconds % (60 * 60)) / 60);
return `${days}d ${hours}h ${minutes}m`;
};
if (isLoading) {
return (
<div className="flex h-[400px] w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (queryError) {
return (
<div className="flex min-h-[55vh] w-full items-center justify-center p-4">
<div className="max-w-xl text-center">
<p className="mb-2 text-base font-medium leading-none text-muted-foreground">
Error fetching metrics{" "}
</p>
<p className="whitespace-pre-line text-sm text-destructive">
{queryError instanceof Error
? queryError.message
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
</p>
<p className=" text-sm text-muted-foreground">URL: {BASE_URL}</p>
</div>
</div>
);
}
return (
<div className="space-y-4 pt-5 pb-10 w-full md:px-4">
<div className="flex items-center justify-between flex-wrap gap-2">
<h2 className="text-2xl font-bold tracking-tight">System Monitoring</h2>
<div className="flex items-center gap-4 flex-wrap">
<div>
<span className="text-sm text-muted-foreground">Data points:</span>
<Select
value={dataPoints}
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
setDataPoints(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select points" />
</SelectTrigger>
<SelectContent>
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<span className="text-sm text-muted-foreground">
Refresh interval:
</span>
<Select
value={refreshInterval}
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
setRefreshInterval(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Uptime</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{formatUptime(metrics.uptime || 0)}
</p>
</div>
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">CPU Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">{metrics.cpu}%</p>
</div>
<div className="rounded-lg border text-card-foreground bg-transparent shadow-sm p-6">
<div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Memory Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{metrics.memUsedGB} GB / {metrics.memTotal} GB
</p>
</div>
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Disk Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">{metrics.diskUsed}%</p>
</div>
</div>
{/* System Information */}
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
<h3 className="text-lg font-medium mb-4">System Information</h3>
<div className="grid gap-4 md:grid-cols-2">
<div>
<h4 className="text-sm font-medium text-muted-foreground">CPU</h4>
<p className="mt-1">{metrics.cpuModel}</p>
<p className="text-sm text-muted-foreground mt-1">
{metrics.cpuPhysicalCores} Physical Cores ({metrics.cpuCores}{" "}
Threads) @ {metrics.cpuSpeed}GHz
</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Operating System
</h4>
<p className="mt-1">{metrics.distro}</p>
<p className="text-sm text-muted-foreground mt-1">
Kernel: {metrics.kernel} ({metrics.arch})
</p>
</div>
</div>
</div>
{/* Charts Grid */}
<div className="grid gap-4 grid-cols-1 md:grid-cols-1 xl:grid-cols-2">
<CPUChart data={historicalData} />
<MemoryChart data={historicalData} />
<DiskChart data={metrics} />
<NetworkChart data={historicalData} />
</div>
</div>
);
};

View File

@@ -1,10 +0,0 @@
import React from "react";
import { DockerMonitoring } from "../docker/show";
export const ShowMonitoring = () => {
return (
<div className="my-6 w-full ">
<DockerMonitoring appName="dokploy" />
</div>
);
};

View File

@@ -70,7 +70,7 @@ interface Props {
export const AddApplication = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
@@ -166,7 +166,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -197,7 +197,12 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -73,6 +73,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
@@ -173,7 +174,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -204,7 +205,12 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -89,7 +89,7 @@ const mySchema = z.discriminatedUnion("type", [
z
.object({
type: z.literal("postgres"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("postgres"),
databaseUser: z.string().default("postgres"),
})
.merge(baseDatabaseSchema),
@@ -110,7 +110,7 @@ const mySchema = z.discriminatedUnion("type", [
type: z.literal("mysql"),
databaseRootPassword: z.string().default(""),
databaseUser: z.string().default("mysql"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("mysql"),
})
.merge(baseDatabaseSchema),
z
@@ -119,7 +119,7 @@ const mySchema = z.discriminatedUnion("type", [
dockerImage: z.string().default("mariadb:4"),
databaseRootPassword: z.string().default(""),
databaseUser: z.string().default("mariadb"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("mariadb"),
})
.merge(baseDatabaseSchema),
]);
@@ -206,7 +206,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
promise = postgresMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "postgres",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
@@ -233,7 +233,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
...commonParams,
databasePassword: data.databasePassword,
databaseRootPassword: data.databaseRootPassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "mariadb",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
@@ -242,7 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
promise = mysqlMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "mysql",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
databaseRootPassword: data.databaseRootPassword,

View File

@@ -80,6 +80,7 @@ export const AddTemplate = ({ projectId }: Props) => {
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const { data } = api.compose.templates.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: tags, isLoading: isLoadingTags } =
api.compose.getTags.useQuery();
@@ -114,26 +115,28 @@ export const AddTemplate = ({ projectId }: Props) => {
</DialogTrigger>
<DialogContent className="max-h-screen sm:max-w-[90vw] p-0">
<DialogHeader className="sticky top-0 z-10 bg-background p-6 border-b">
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col space-y-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
<div>
<DialogTitle>Create from Template</DialogTitle>
<DialogDescription>
Create an open source application from a template
</DialogDescription>
</div>
<div className="flex items-center gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<Input
placeholder="Search Template"
onChange={(e) => setQuery(e.target.value)}
className="w-[200px]"
className="w-full sm:w-[200px]"
value={query}
/>
<Popover modal={true}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn("w-[200px] justify-between !bg-input")}
className={cn(
"w-full sm:w-[200px] justify-between !bg-input",
)}
>
{isLoadingTags
? "Loading...."
@@ -224,7 +227,11 @@ export const AddTemplate = ({ projectId }: Props) => {
<ScrollArea className="h-[calc(98vh-8rem)]">
<div className="p-6">
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{isError && (
<AlertBlock type="error" className="mb-4">
{error?.message}
</AlertBlock>
)}
{templates.length === 0 ? (
<div className="flex justify-center items-center w-full gap-2 min-h-[50vh]">
@@ -238,8 +245,8 @@ export const AddTemplate = ({ projectId }: Props) => {
className={cn(
"grid gap-6",
viewMode === "detailed"
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-6"
: "grid-cols-2 sm:grid-cols-4 lg:grid-cols-8",
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6",
)}
>
{templates?.map((template, index) => (
@@ -302,7 +309,7 @@ export const AddTemplate = ({ projectId }: Props) => {
{/* Create Button */}
<div
className={cn(
"flex-none px-6 pb-6 pt-3 mt-auto",
"flex-none px-6 py-3 mt-auto",
viewMode === "detailed"
? "flex items-center justify-between bg-muted/30 border-t"
: "flex justify-center",
@@ -340,6 +347,7 @@ export const AddTemplate = ({ projectId }: Props) => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
size="sm"
className={cn(
"w-auto",
@@ -365,7 +373,8 @@ export const AddTemplate = ({ projectId }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server (Optional)
Select a Server{" "}
{!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
@@ -398,7 +407,12 @@ export const AddTemplate = ({ projectId }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>

View File

@@ -118,7 +118,7 @@ export const HandleProject = ({ projectId }: Props) => {
</DialogTrigger>
<DialogContent className="sm:m:max-w-lg ">
<DialogHeader>
<DialogTitle>Add a project</DialogTitle>
<DialogTitle>{projectId ? "Update" : "Add a"} project</DialogTitle>
<DialogDescription>The home of something big!</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}

View File

@@ -99,8 +99,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
<DialogTitle>Project Environment</DialogTitle>
<DialogDescription>
Update the env Environment variables that are accessible to all
services of this project. Use this syntax to reference project-level
variables in your service environments:
services of this project.
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}

View File

@@ -23,8 +23,10 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
@@ -75,10 +77,10 @@ export const ShowProjects = () => {
list={[{ name: "Projects", href: "/dashboard/projects" }]}
/>
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md ">
<div className="flex justify-between gap-4 w-full items-center">
<CardHeader className="">
<div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
<CardHeader className="p-0">
<CardTitle className="text-xl flex flex-row gap-2">
<FolderInput className="size-6 text-muted-foreground self-center" />
Projects
@@ -87,9 +89,12 @@ export const ShowProjects = () => {
Create and manage your projects
</CardDescription>
</CardHeader>
<div className=" px-4 ">
<HandleProject />
</div>
{(auth?.rol === "admin" || user?.canCreateProjects) && (
<div className="">
<HandleProject />
</div>
)}
</div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
@@ -117,7 +122,7 @@ export const ShowProjects = () => {
</span>
</div>
)}
<div className="w-full grid sm:grid-cols-2 lg:grid-cols-4 flex-wrap gap-5">
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 flex-wrap gap-5">
{filteredProjects?.map((project) => {
const emptyServices =
project?.mariadb.length === 0 &&
@@ -145,15 +150,92 @@ export const ShowProjects = () => {
<Link
href={`/dashboard/project/${project.projectId}`}
>
<Card className="group relative w-full bg-transparent transition-colors hover:bg-border">
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
{project.applications.length > 0 ||
project.compose.length > 0 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
onClick={(e) => e.stopPropagation()}
>
{project.applications.length > 0 && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Applications
</DropdownMenuLabel>
{project.applications.map((app) => (
<div key={app.applicationId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs">
{app.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{app.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
))}
</DropdownMenuGroup>
)}
{project.compose.length > 0 && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Compose
</DropdownMenuLabel>
{project.compose.map((comp) => (
<div key={comp.composeId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs">
{comp.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{comp.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
))}
</DropdownMenuGroup>
)}
</DropdownMenuContent>
</DropdownMenu>
) : null}
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2">
<span className="flex flex-col gap-1.5">
@@ -179,7 +261,10 @@ export const ShowProjects = () => {
<MoreHorizontalIcon className="size-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[200px] space-y-2">
<DropdownMenuContent
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuLabel className="font-normal">
Actions
</DropdownMenuLabel>

View File

@@ -168,6 +168,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>

View File

@@ -61,6 +61,7 @@ export const AddCertificate = () => {
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync, isError, error, isLoading } =
api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
@@ -181,7 +182,7 @@ export const AddCertificate = () => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud && "(Optional)"}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -202,7 +203,12 @@ export const AddCertificate = () => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -50,7 +50,7 @@ export const ShowCertificates = () => {
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<ShieldCheck className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground">
<span className="text-base text-muted-foreground text-center">
You don't have any certificates created
</span>
<AddCertificate />

View File

@@ -4,7 +4,7 @@ import {
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -13,8 +13,16 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { formatDate } from "date-fns";
import { GitBranch, Loader2, Trash2 } from "lucide-react";
import {
ExternalLinkIcon,
GitBranch,
ImportIcon,
Loader2,
Trash2,
} from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
@@ -27,7 +35,20 @@ export const ShowGitProviders = () => {
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
const { mutateAsync, isLoading: isRemoving } =
api.gitProvider.remove.useMutation();
const url = useUrl();
const getGitlabUrl = (
clientId: string,
gitlabId: string,
gitlabUrl: string,
) => {
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
const scope = "api read_user read_repository";
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
return authUrl;
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
@@ -128,6 +149,51 @@ export const ShowGitProviders = () => {
</div>
<div className="flex flex-row gap-1">
{!haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1">
<Link
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
className={buttonVariants({
size: "icon",
variant: "ghost",
})}
>
<ImportIcon className="size-4 text-primary" />
</Link>
</div>
)}
{haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1">
<Link
href={`${gitProvider?.github?.githubAppName}`}
target="_blank"
className={buttonVariants({
size: "icon",
variant: "ghost",
})}
>
<ExternalLinkIcon className="size-4 text-primary" />
</Link>
</div>
)}
{!haveGitlabRequirements && isGitlab && (
<div className="flex flex-col gap-1">
<Link
href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "",
gitProvider.gitlab?.gitlabId || "",
gitProvider.gitlab?.gitlabUrl,
)}
target="_blank"
className={buttonVariants({
size: "icon",
variant: "ghost",
})}
>
<ImportIcon className="size-4 text-primary" />
</Link>
</div>
)}
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github.githubId}

View File

@@ -28,7 +28,13 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react";
import {
AlertTriangle,
Mail,
MessageCircleMore,
PenBoxIcon,
PlusIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -43,6 +49,7 @@ const notificationBaseSchema = z.object({
databaseBackup: z.boolean().default(false),
dokployRestart: z.boolean().default(false),
dockerCleanup: z.boolean().default(false),
serverThreshold: z.boolean().default(false),
});
export const notificationSchema = z.discriminatedUnion("type", [
@@ -84,6 +91,15 @@ export const notificationSchema = z.discriminatedUnion("type", [
.min(1, { message: "At least one email is required" }),
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("gotify"),
serverUrl: z.string().min(1, { message: "Server URL is required" }),
appToken: z.string().min(1, { message: "App Token is required" }),
priority: z.number().min(1).max(10).default(5),
decoration: z.boolean().default(true),
})
.merge(notificationBaseSchema),
]);
export const notificationsMap = {
@@ -103,6 +119,10 @@ export const notificationsMap = {
icon: <Mail size={29} className="text-muted-foreground" />,
label: "Email",
},
gotify: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
label: "Gotify",
},
};
export type NotificationSchema = z.infer<typeof notificationSchema>;
@@ -126,13 +146,14 @@ export const HandleNotifications = ({ notificationId }: Props) => {
);
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
api.notification.testSlackConnection.useMutation();
const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } =
api.notification.testTelegramConnection.useMutation();
const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } =
api.notification.testDiscordConnection.useMutation();
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
api.notification.testEmailConnection.useMutation();
const { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
api.notification.testGotifyConnection.useMutation();
const slackMutation = notificationId
? api.notification.updateSlack.useMutation()
: api.notification.createSlack.useMutation();
@@ -145,6 +166,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const emailMutation = notificationId
? api.notification.updateEmail.useMutation()
: api.notification.createEmail.useMutation();
const gotifyMutation = notificationId
? api.notification.updateGotify.useMutation()
: api.notification.createGotify.useMutation();
const form = useForm<NotificationSchema>({
defaultValues: {
@@ -181,6 +205,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
channel: notification.slack?.channel || "",
name: notification.name,
type: notification.notificationType,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "telegram") {
form.reset({
@@ -193,6 +218,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
type: notification.notificationType,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "discord") {
form.reset({
@@ -205,6 +231,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
decoration: notification.discord?.decoration || undefined,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "email") {
form.reset({
@@ -221,6 +248,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
fromAddress: notification.email?.fromAddress,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "gotify") {
form.reset({
appBuildError: notification.appBuildError,
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
type: notification.notificationType,
appToken: notification.gotify?.appToken,
decoration: notification.gotify?.decoration || undefined,
priority: notification.gotify?.priority,
serverUrl: notification.gotify?.serverUrl,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
});
}
} else {
@@ -233,6 +275,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
telegram: telegramMutation,
discord: discordMutation,
email: emailMutation,
gotify: gotifyMutation,
};
const onSubmit = async (data: NotificationSchema) => {
@@ -242,6 +285,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dokployRestart,
databaseBackup,
dockerCleanup,
serverThreshold,
} = data;
let promise: Promise<unknown> | null = null;
if (data.type === "slack") {
@@ -256,6 +300,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
slackId: notification?.slackId || "",
notificationId: notificationId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "telegram") {
promise = telegramMutation.mutateAsync({
@@ -269,6 +314,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
telegramId: notification?.telegramId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "discord") {
promise = discordMutation.mutateAsync({
@@ -282,6 +328,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
discordId: notification?.discordId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "email") {
promise = emailMutation.mutateAsync({
@@ -299,6 +346,22 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
emailId: notification?.emailId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "gotify") {
promise = gotifyMutation.mutateAsync({
appBuildError: appBuildError,
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
serverUrl: data.serverUrl,
appToken: data.appToken,
priority: data.priority,
name: data.name,
dockerCleanup: dockerCleanup,
decoration: data.decoration,
notificationId: notificationId || "",
gotifyId: notification?.gotifyId || "",
});
}
@@ -700,6 +763,94 @@ export const HandleNotifications = ({ notificationId }: Props) => {
</Button>
</>
)}
{type === "gotify" && (
<>
<FormField
control={form.control}
name="serverUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Server URL</FormLabel>
<FormControl>
<Input
placeholder="https://gotify.example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appToken"
render={({ field }) => (
<FormItem>
<FormLabel>App Token</FormLabel>
<FormControl>
<Input
placeholder="AzxcvbnmKjhgfdsa..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
defaultValue={5}
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Priority</FormLabel>
<FormControl>
<Input
placeholder="5"
{...field}
onChange={(e) => {
const value = e.target.value;
if (value) {
const port = Number.parseInt(value);
if (port > 0 && port < 10) {
field.onChange(port);
}
}
}}
type="number"
/>
</FormControl>
<FormDescription>
Message priority (1-10, default: 5)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="decoration"
defaultValue={true}
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Decoration</FormLabel>
<FormDescription>
Decorate the notification with emojis.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
</div>
</div>
<div className="flex flex-col gap-4">
@@ -814,6 +965,30 @@ export const HandleNotifications = ({ notificationId }: Props) => {
)}
/>
)}
{isCloud && (
<FormField
control={form.control}
name="serverThreshold"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Server Threshold</FormLabel>
<FormDescription>
Trigger the action when the server threshold is
reached.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
</div>
</form>
@@ -824,7 +999,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingSlack ||
isLoadingTelegram ||
isLoadingDiscord ||
isLoadingEmail
isLoadingEmail ||
isLoadingGotify
}
variant="secondary"
onClick={async () => {
@@ -853,6 +1029,13 @@ export const HandleNotifications = ({ notificationId }: Props) => {
toAddresses: form.getValues("toAddresses"),
fromAddress: form.getValues("fromAddress"),
});
} else if (type === "gotify") {
await testGotifyConnection({
serverUrl: form.getValues("serverUrl"),
appToken: form.getValues("appToken"),
priority: form.getValues("priority"),
decoration: form.getValues("decoration"),
});
}
toast.success("Connection Success");
} catch (err) {

View File

@@ -13,7 +13,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Bell, Loader2, Mail, Trash2 } from "lucide-react";
import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { HandleNotifications } from "./handle-notifications";
@@ -47,7 +47,7 @@ export const ShowNotifications = () => {
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<Bell />
<span className="text-base text-muted-foreground">
<span className="text-base text-muted-foreground text-center">
To send notifications it is required to set at least 1
provider.
</span>
@@ -83,6 +83,11 @@ export const ShowNotifications = () => {
<Mail className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
</div>
)}
{notification.name}
</span>

View File

@@ -0,0 +1,636 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input, NumberInput } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { extractServices } from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { Eye, EyeOff, LayoutDashboardIcon, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props {
serverId?: string;
}
const Schema = z.object({
metricsConfig: z.object({
server: z.object({
refreshRate: z.number().min(2, {
message: "Server Refresh Rate is required",
}),
port: z.number().min(1, {
message: "Port is required",
}),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number().min(1, {
message: "Retention days must be at least 1",
}),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
cronJob: z.string().min(1, {
message: "Cron Job is required",
}),
}),
containers: z.object({
refreshRate: z.number().min(2, {
message: "Container Refresh Rate is required",
}),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
}),
});
type Schema = z.infer<typeof Schema>;
export const SetupMonitoring = ({ serverId }: Props) => {
const { data, isLoading } = serverId
? api.server.one.useQuery(
{
serverId: serverId || "",
},
{
enabled: !!serverId,
},
)
: api.admin.one.useQuery();
const url = useUrl();
const { data: projects } = api.project.all.useQuery();
const extractServicesFromProjects = (projects: any[] | undefined) => {
if (!projects) return [];
const allServices = projects.flatMap((project) => {
const services = extractServices(project);
return serverId
? services
.filter((service) => service.serverId === serverId)
.map((service) => service.appName)
: services.map((service) => service.appName);
});
return [...new Set(allServices)];
};
const services = extractServicesFromProjects(projects);
const form = useForm<Schema>({
resolver: zodResolver(Schema),
defaultValues: {
metricsConfig: {
server: {
refreshRate: 20,
port: 4500,
token: "",
urlCallback: `${url}/api/trpc/notification.receiveNotification`,
retentionDays: 7,
thresholds: {
cpu: 0,
memory: 0,
},
cronJob: "",
},
containers: {
refreshRate: 20,
services: {
include: [],
exclude: [],
},
},
},
},
});
useEffect(() => {
if (data) {
form.reset({
metricsConfig: {
server: {
refreshRate: data?.metricsConfig?.server?.refreshRate,
port: data?.metricsConfig?.server?.port,
token: data?.metricsConfig?.server?.token || generateToken(),
urlCallback:
data?.metricsConfig?.server?.urlCallback ||
`${url}/api/trpc/notification.receiveNotification`,
retentionDays: data?.metricsConfig?.server?.retentionDays || 5,
thresholds: {
cpu: data?.metricsConfig?.server?.thresholds?.cpu,
memory: data?.metricsConfig?.server?.thresholds?.memory,
},
cronJob: data?.metricsConfig?.server?.cronJob || "0 0 * * *",
},
containers: {
refreshRate: data?.metricsConfig?.containers?.refreshRate,
services: {
include: data?.metricsConfig?.containers?.services?.include,
exclude: data?.metricsConfig?.containers?.services?.exclude,
},
},
},
});
}
}, [data, url]);
const [search, setSearch] = useState("");
const [searchExclude, setSearchExclude] = useState("");
const [showToken, setShowToken] = useState(false);
const availableServices = services?.filter(
(service) =>
!form
.watch("metricsConfig.containers.services.include")
?.some((s) => s === service) &&
!form
.watch("metricsConfig.containers.services.exclude")
?.includes(service) &&
service.toLowerCase().includes(search.toLowerCase()),
);
const availableServicesToExclude = [
...(services?.filter(
(service) =>
!form
.watch("metricsConfig.containers.services.exclude")
?.includes(service) &&
!form
.watch("metricsConfig.containers.services.include")
?.some((s) => s === service) &&
service.toLowerCase().includes(searchExclude.toLowerCase()),
) ?? []),
...(!form.watch("metricsConfig.containers.services.exclude")?.includes("*")
? ["*"]
: []),
];
const { mutateAsync } = serverId
? api.server.setupMonitoring.useMutation()
: api.admin.setupMonitoring.useMutation();
const generateToken = () => {
const array = new Uint8Array(64);
crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
"",
);
};
const onSubmit = async (values: Schema) => {
await mutateAsync({
serverId: serverId || "",
metricsConfig: values.metricsConfig,
})
.then(() => {
toast.success("Server updated successfully");
})
.catch(() => {
toast.error("Error updating the server");
});
};
return (
<>
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<LayoutDashboardIcon className="size-6 text-muted-foreground self-center" />
Monitoring
</CardTitle>
<CardDescription>
Monitor your servers and containers in realtime with notifications
when they reach their thresholds.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6 py-6 border-t">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-full flex-col gap-4"
>
<AlertBlock>
Using a lower refresh rate will make your CPU and memory usage
higher, we recommend 30-60 seconds
</AlertBlock>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="metricsConfig.server.refreshRate"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Server Refresh Rate</FormLabel>
<FormControl>
<NumberInput placeholder="10" {...field} />
</FormControl>
<FormDescription>
Please set the refresh rate for the server in seconds
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.containers.refreshRate"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Container Refresh Rate</FormLabel>
<FormControl>
<NumberInput placeholder="10" {...field} />
</FormControl>
<FormDescription>
Please set the refresh rate for the containers in seconds
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.cronJob"
render={({ field }) => (
<FormItem>
<FormLabel>Cron Job</FormLabel>
<FormControl>
<Input {...field} placeholder="0 0 * * *" />
</FormControl>
<FormDescription>
Cron job for cleaning up metrics
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.retentionDays"
render={({ field }) => (
<FormItem>
<FormLabel>Server Retention Days</FormLabel>
<FormControl>
<NumberInput {...field} />
</FormControl>
<FormDescription>
Number of days to retain server metrics data
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.port"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Port</FormLabel>
<FormControl>
<NumberInput placeholder="4500" {...field} />
</FormControl>
<FormDescription>
Please set the port for the metrics server
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.containers.services.include"
render={({ field }) => (
<FormItem>
<FormLabel>Include Services</FormLabel>
<FormControl>
<div className="flex flex-col gap-4">
<div className="flex gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Add Service</Button>
</PopoverTrigger>
<PopoverContent
className="w-[300px] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search service..."
value={search}
onValueChange={setSearch}
/>
{availableServices?.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground">
No services available.
</div>
) : (
<>
<CommandEmpty>
No service found.
</CommandEmpty>
<CommandGroup>
{availableServices?.map((service) => (
<CommandItem
key={service}
value={service}
onSelect={() => {
field.onChange([
...(field.value ?? []),
service,
]);
setSearch("");
}}
>
{service}
</CommandItem>
))}
</CommandGroup>
</>
)}
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-wrap gap-2">
{field.value?.map((service) => (
<Badge
key={service}
variant="secondary"
className="flex items-center gap-2"
>
{service}
<Button
type="button"
variant="ghost"
size="icon"
className="h-4 w-4 p-0"
onClick={() => {
field.onChange(
field.value?.filter((s) => s !== service),
);
}}
>
×
</Button>
</Badge>
))}
<FormDescription>
Services to monitor.
</FormDescription>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.containers.services.exclude"
render={({ field }) => (
<FormItem>
<FormLabel>Exclude Services</FormLabel>
<FormControl>
<div className="flex flex-col gap-4">
<div className="flex gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Add Service</Button>
</PopoverTrigger>
<PopoverContent
className="w-[300px] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search service..."
value={searchExclude}
onValueChange={setSearchExclude}
/>
{availableServicesToExclude?.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground">
No services available.
</div>
) : (
<>
<CommandEmpty>
No service found.
</CommandEmpty>
<CommandGroup>
{availableServicesToExclude.map(
(service) => (
<CommandItem
key={service}
value={service}
onSelect={() => {
field.onChange([
...(field.value ?? []),
service,
]);
setSearchExclude("");
}}
>
{service}
</CommandItem>
),
)}
</CommandGroup>
</>
)}
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-wrap gap-2">
{field.value?.map((service, index) => (
<Badge
key={service}
variant="secondary"
className="flex items-center gap-2"
>
{service}
<Button
type="button"
variant="ghost"
size="icon"
className="h-4 w-4 p-0"
onClick={() => {
field.onChange(
field.value?.filter((_, i) => i !== index),
);
}}
>
×
</Button>
</Badge>
))}
<FormDescription>
Services to exclude from monitoring
</FormDescription>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.thresholds.cpu"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Threshold (%)</FormLabel>
<FormControl>
<NumberInput {...field} />
</FormControl>
<FormDescription>
Alert when CPU usage exceeds this percentage
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.thresholds.memory"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Threshold (%)</FormLabel>
<FormControl>
<NumberInput {...field} />
</FormControl>
<FormDescription>
Alert when memory usage exceeds this percentage
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.token"
render={({ field }) => (
<FormItem>
<FormLabel>Metrics Token</FormLabel>
<FormControl>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
type={showToken ? "text" : "password"}
placeholder="Enter your metrics token"
{...field}
/>
<Button
type="button"
variant="secondary"
size="icon"
className="absolute right-0 top-1/2 -translate-y-1/2"
onClick={() => setShowToken(!showToken)}
title={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const newToken = generateToken();
form.setValue(
"metricsConfig.server.token",
newToken,
);
toast.success("Token generated successfully");
}}
title="Generate new token"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</FormControl>
<FormDescription>
Token for authenticating metrics requests
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.urlCallback"
render={({ field }) => (
<FormItem>
<FormLabel>Metrics Callback URL</FormLabel>
<FormControl>
<Input
placeholder="https://your-callback-url.com"
{...field}
/>
</FormControl>
<FormDescription>
URL where metrics will be sent
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex items-center justify-end gap-2">
<Button type="submit" isLoading={form.formState.isSubmitting}>
Save changes
</Button>
</div>
</form>
</Form>
</CardContent>
</>
);
};

View File

@@ -19,6 +19,7 @@ import {
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { CopyIcon, ExternalLinkIcon, ServerIcon } from "lucide-react";
@@ -30,6 +31,7 @@ import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { EditScript } from "./edit-script";
import { GPUSupport } from "./gpu-support";
import { SecurityAudit } from "./security-audit";
import { SetupMonitoring } from "./setup-monitoring";
import { ValidateServer } from "./validate-server";
interface Props {
@@ -48,7 +50,7 @@ export const SetupServer = ({ serverId }: Props) => {
);
const [activeLog, setActiveLog] = useState<string | null>(null);
const { data: isCloud } = api.settings.isCloud.useQuery();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
@@ -112,11 +114,19 @@ export const SetupServer = ({ serverId }: Props) => {
</AlertBlock>
<Tabs defaultValue="ssh-keys">
<TabsList className="grid grid-cols-5 w-[700px]">
<TabsList
className={cn(
"grid w-[700px]",
isCloud ? "grid-cols-6" : "grid-cols-5",
)}
>
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="validate">Validate</TabsTrigger>
<TabsTrigger value="audit">Security</TabsTrigger>
{isCloud && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
</TabsList>
<TabsContent
@@ -309,6 +319,16 @@ export const SetupServer = ({ serverId }: Props) => {
<SecurityAudit serverId={serverId} />
</div>
</TabsContent>
<TabsContent
value="monitoring"
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
>
<div className="flex flex-col gap-2 text-sm pt-3">
<div className="rounded-xl bg-background shadow-md border">
<SetupMonitoring serverId={serverId} />
</div>
</div>
</TabsContent>
<TabsContent
value="gpu-setup"
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"

View File

@@ -0,0 +1,31 @@
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { ShowPaidMonitoring } from "../../monitoring/paid/servers/show-paid-monitoring";
interface Props {
url: string;
token: string;
}
export const ShowMonitoringModal = ({ url, token }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Monitoring
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
<div className="flex gap-4 py-4 w-full">
<ShowPaidMonitoring BASE_URL={url} token={token} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -38,6 +38,7 @@ import { ShowServerActions } from "./actions/show-server-actions";
import { HandleServers } from "./handle-servers";
import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
@@ -48,9 +49,7 @@ export const ShowServers = () => {
const query = router.query;
const { data, refetch, isLoading } = api.server.all.useQuery();
const { mutateAsync } = api.server.remove.useMutation();
const { data: sshKeys } = {
data: [],
};
const { data: sshKeys } = api.sshKey.all.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: canCreateMoreServers } =
api.stripe.canCreateMoreServers.useQuery();
@@ -316,6 +315,16 @@ export const ShowServers = () => {
<ShowDockerContainersModal
serverId={server.serverId}
/>
{isCloud && (
<ShowMonitoringModal
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
token={
server?.metricsConfig?.server
?.token
}
/>
)}
<ShowSwarmOverviewModal
serverId={server.serverId}
/>

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { sshKeyCreate, type sshKeyType } from "@/server/db/validations";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { DownloadIcon, PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -111,6 +111,26 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
toast.error("Error generating the SSH Key");
});
const downloadKey = (
content: string,
defaultFilename: string,
keyType: "private" | "public",
) => {
const keyName = form.watch("name");
const filename = keyName
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultFilename}`
: `${keyType}_${defaultFilename}`;
const blob = new Blob([content], { type: "text/plain" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
@@ -245,7 +265,41 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
</FormItem>
)}
/>
<DialogFooter>
<DialogFooter className="flex items-center justify-between">
<div className="flex items-center gap-4">
{form.watch("privateKey") && (
<Button
type="button"
variant="outline"
size="default"
onClick={() =>
downloadKey(form.watch("privateKey"), "id_rsa", "private")
}
className="flex items-center gap-2"
>
<DownloadIcon className="h-4 w-4" />
Private Key
</Button>
)}
{form.watch("publicKey") && (
<Button
type="button"
variant="outline"
size="default"
onClick={() =>
downloadKey(
form.watch("publicKey"),
"id_rsa.pub",
"public",
)
}
className="flex items-center gap-2"
>
<DownloadIcon className="h-4 w-4" />
Public Key
</Button>
)}
</div>
<Button isLoading={isLoading} type="submit">
{sshKeyId ? "Update" : "Create"}
</Button>

View File

@@ -30,8 +30,8 @@ import { toast } from "sonner";
import { z } from "zod";
const addPermissions = z.object({
accesedProjects: z.array(z.string()).optional(),
accesedServices: z.array(z.string()).optional(),
accessedProjects: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional().default(false),
canCreateServices: z.boolean().optional().default(false),
canDeleteProjects: z.boolean().optional().default(false),
@@ -66,8 +66,8 @@ export const AddUserPermissions = ({ userId }: Props) => {
const form = useForm<AddPermissions>({
defaultValues: {
accesedProjects: [],
accesedServices: [],
accessedProjects: [],
accessedServices: [],
},
resolver: zodResolver(addPermissions),
});
@@ -75,8 +75,8 @@ export const AddUserPermissions = ({ userId }: Props) => {
useEffect(() => {
if (data) {
form.reset({
accesedProjects: data.accesedProjects || [],
accesedServices: data.accesedServices || [],
accessedProjects: data.accessedProjects || [],
accessedServices: data.accessedServices || [],
canCreateProjects: data.canCreateProjects,
canCreateServices: data.canCreateServices,
canDeleteProjects: data.canDeleteProjects,
@@ -98,8 +98,8 @@ export const AddUserPermissions = ({ userId }: Props) => {
canDeleteServices: data.canDeleteServices,
canDeleteProjects: data.canDeleteProjects,
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
accesedProjects: data.accesedProjects || [],
accesedServices: data.accesedServices || [],
accessedProjects: data.accessedProjects || [],
accessedServices: data.accessedServices || [],
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
@@ -318,7 +318,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
/>
<FormField
control={form.control}
name="accesedProjects"
name="accessedProjects"
render={() => (
<FormItem className="md:col-span-2">
<div className="mb-4">
@@ -339,7 +339,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
<FormField
key={`project-${index}`}
control={form.control}
name="accesedProjects"
name="accessedProjects"
render={({ field }) => {
return (
<FormItem
@@ -380,7 +380,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
<FormField
key={`project-${index}`}
control={form.control}
name="accesedServices"
name="accessedServices"
render={({ field }) => {
return (
<FormItem

View File

@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css";
import { AttachAddon } from "@xterm/addon-attach";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { useTheme } from "next-themes";
import { getLocalServerData } from "./local-server-config";
@@ -37,6 +38,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
foreground: "currentColor",
},
});
const addonFit = new FitAddon();
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
@@ -54,6 +56,8 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
const ws = new WebSocket(wsUrl);
const addonAttach = new AttachAddon(ws);
const clipboardAddon = new ClipboardAddon();
term.loadAddon(clipboardAddon);
// @ts-ignore
term.open(termRef.current);
@@ -68,7 +72,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
return (
<div className="flex flex-col gap-4">
<div className="w-full h-full bg-transparent border rounded-lg p-2 ">
<div className="w-full h-full bg-transparent border rounded-lg p-2">
<div id={id} ref={termRef} className="rounded-xl" />
</div>
</div>

View File

@@ -60,7 +60,7 @@ export function NodeCard({ node, serverId }: Props) {
<div className="font-medium">{node.Hostname}</div>
<Badge variant="green">{node.ManagerStatus || "Worker"}</Badge>
</div>
<div className="flex flex-wrap items-center space-x-4">
<div className="flex flex-wrap items-center gap-4">
<Badge variant="green">TLS Status: {node.TLSStatus}</Badge>
<Badge variant="blue">Availability: {node.Availability}</Badge>
</div>

View File

@@ -70,9 +70,9 @@ export default function SwarmMonitorCard({ serverId }: Props) {
);
return (
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
<header className="flex items-center justify-between">
<header className="flex items-center flex-wrap gap-4 justify-between">
<div className="space-y-1">
<CardTitle className="text-xl flex flex-row gap-2">
<WorkflowIcon className="size-6 text-muted-foreground self-center" />
@@ -94,7 +94,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
)}
</header>
<div className="grid gap-6 md:grid-cols-3">
<div className="grid gap-6 lg:grid-cols-3">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Nodes</CardTitle>

View File

@@ -5,9 +5,5 @@ interface Props {
}
export const DashboardLayout = ({ children }: Props) => {
return (
<Page>
<div>{children}</div>
</Page>
);
return <Page>{children}</Page>;
};

View File

@@ -11,46 +11,27 @@ interface Props {
export const OnboardingLayout = ({ children }: Props) => {
return (
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
<div className="relative hidden h-full flex-col p-10 text-white dark:border-r lg:flex">
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
<div className="absolute inset-0 bg-muted" />
<Link
href="https://dokploy.com"
className="relative z-20 flex items-center text-lg font-medium gap-4"
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
>
<Logo className="size-10" />
Dokploy
</Link>
<div className="relative z-20 mt-auto">
<blockquote className="space-y-2">
<p className="text-lg">
<p className="text-lg text-primary">
&ldquo;The Open Source alternative to Netlify, Vercel,
Heroku.&rdquo;
</p>
{/* <footer className="text-sm">Sofia Davis</footer> */}
</blockquote>
</div>
</div>
<div className="w-full">
<div className="flex w-full flex-col justify-center space-y-6 max-w-lg mx-auto">
{children}
{/* <p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</Link>
.
</p> */}
</div>
<div className="flex items-center gap-4 justify-center absolute bottom-4 right-4 text-muted-foreground">
<Button variant="ghost" size="icon">

View File

@@ -5,9 +5,5 @@ interface Props {
}
export const ProjectLayout = ({ children }: Props) => {
return (
<div>
<Page>{children}</Page>
</div>
);
return <Page>{children}</Page>;
};

File diff suppressed because it is too large Load Diff

View File

@@ -118,14 +118,16 @@ export const UserNav = () => {
</DropdownMenuItem>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings/server");
}}
>
Settings
</DropdownMenuItem>
{data?.rol === "admin" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</>
) : (
<>
@@ -147,6 +149,17 @@ export const UserNav = () => {
Servers
</DropdownMenuItem>
)}
{data?.rol === "admin" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</>
)}
</DropdownMenuGroup>

View File

@@ -28,7 +28,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
<BreadcrumbList>
{list.map((item, index) => (
<Fragment key={item.name}>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbItem className="block">
<BreadcrumbLink href={item.href} asChild={!!item.href}>
{item.href ? (
<Link href={item.href}>{item.name}</Link>
@@ -37,7 +37,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
)}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbSeparator className="block" />
</Fragment>
))}
</BreadcrumbList>

View File

@@ -67,7 +67,7 @@ ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color,
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {

View File

@@ -17,7 +17,7 @@ import {
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
const SIDEBAR_COOKIE_NAME = "sidebar:state";
export const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
@@ -231,7 +231,7 @@ const Sidebar = React.forwardRef<
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-out",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
@@ -241,7 +241,7 @@ const Sidebar = React.forwardRef<
/>
<div
className={cn(
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-out md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
@@ -329,7 +329,7 @@ const SidebarInset = React.forwardRef<
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"relative flex min-h-svh overflow-auto w-full flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}

View File

@@ -0,0 +1,15 @@
ALTER TYPE "notificationType" ADD VALUE 'gotify';--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "gotify" (
"gotifyId" text PRIMARY KEY NOT NULL,
"serverUrl" text NOT NULL,
"appToken" text NOT NULL,
"priority" integer DEFAULT 5 NOT NULL,
"decoration" boolean
);
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "gotifyId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_gotifyId_gotify_gotifyId_fk" FOREIGN KEY ("gotifyId") REFERENCES "public"."gotify"("gotifyId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "admin" ADD COLUMN "cleanupCacheApplications" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "cleanupCacheOnCompose" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "deployment" ADD COLUMN "errorMessage" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "admin" ALTER COLUMN "cleanupCacheApplications" SET DEFAULT false;

View File

@@ -0,0 +1,3 @@
-- Custom SQL migration file, put you code below!
UPDATE "admin" SET "cleanupCacheApplications" = false;

View File

@@ -0,0 +1,4 @@
ALTER TABLE "admin" ADD COLUMN "enablePaidFeatures" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "metricsConfig" jsonb DEFAULT '{"server":{"refreshRate":20,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":20,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL;--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "serverThreshold" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "server" ADD COLUMN "metricsConfig" jsonb DEFAULT '{"server":{"refreshRate":20,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":20,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "admin" ALTER COLUMN "metricsConfig" SET DEFAULT '{"server":{"type":"Dokploy","refreshRate":20,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":20,"services":{"include":[],"exclude":[]}}}'::jsonb;--> statement-breakpoint
ALTER TABLE "server" ALTER COLUMN "metricsConfig" SET DEFAULT '{"server":{"type":"Remote","refreshRate":20,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":20,"services":{"include":[],"exclude":[]}}}'::jsonb;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "admin" ALTER COLUMN "metricsConfig" SET DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb;--> statement-breakpoint
ALTER TABLE "server" ALTER COLUMN "metricsConfig" SET DEFAULT '{"server":{"type":"Remote","refreshRate":60,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb;

View File

@@ -1,5 +1,5 @@
{
"version": "6",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.application": {
@@ -252,7 +252,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.postgres": {
"name": "postgres",
@@ -393,7 +396,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.user": {
"name": "user",
@@ -522,7 +528,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.admin": {
"name": "admin",
@@ -638,7 +647,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.auth": {
"name": "auth",
@@ -692,7 +704,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.project": {
"name": "project",
@@ -746,7 +761,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.domain": {
"name": "domain",
@@ -828,7 +846,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mariadb": {
"name": "mariadb",
@@ -975,7 +996,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mongo": {
"name": "mongo",
@@ -1110,7 +1134,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mysql": {
"name": "mysql",
@@ -1257,7 +1284,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.backup": {
"name": "backup",
@@ -1399,7 +1429,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.destination": {
"name": "destination",
@@ -1471,7 +1504,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.deployment": {
"name": "deployment",
@@ -1532,7 +1568,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mount": {
"name": "mount",
@@ -1700,7 +1739,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.certificate": {
"name": "certificate",
@@ -1754,7 +1796,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.session": {
"name": "session",
@@ -1796,7 +1841,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redirect": {
"name": "redirect",
@@ -1863,7 +1911,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.security": {
"name": "security",
@@ -1926,7 +1977,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.port": {
"name": "port",
@@ -1980,7 +2034,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redis": {
"name": "redis",
@@ -2109,7 +2166,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
}
},
"enums": {
@@ -2214,5 +2274,9 @@
"columns": {}
},
"id": "c6215051-7cd1-412d-b8df-b50d58acacff",
"prevId": "00000000-0000-0000-0000-000000000000"
"prevId": "00000000-0000-0000-0000-000000000000",
"sequences": {},
"policies": {},
"views": {},
"roles": {}
}

View File

@@ -1,5 +1,5 @@
{
"version": "6",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.application": {
@@ -252,7 +252,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.postgres": {
"name": "postgres",
@@ -393,7 +396,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.user": {
"name": "user",
@@ -522,7 +528,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.admin": {
"name": "admin",
@@ -638,7 +647,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.auth": {
"name": "auth",
@@ -698,7 +710,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.project": {
"name": "project",
@@ -752,7 +767,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.domain": {
"name": "domain",
@@ -834,7 +852,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mariadb": {
"name": "mariadb",
@@ -981,7 +1002,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mongo": {
"name": "mongo",
@@ -1116,7 +1140,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mysql": {
"name": "mysql",
@@ -1263,7 +1290,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.backup": {
"name": "backup",
@@ -1405,7 +1435,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.destination": {
"name": "destination",
@@ -1477,7 +1510,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.deployment": {
"name": "deployment",
@@ -1538,7 +1574,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mount": {
"name": "mount",
@@ -1706,7 +1745,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.certificate": {
"name": "certificate",
@@ -1760,7 +1802,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.session": {
"name": "session",
@@ -1802,7 +1847,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redirect": {
"name": "redirect",
@@ -1869,7 +1917,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.security": {
"name": "security",
@@ -1932,7 +1983,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.port": {
"name": "port",
@@ -1986,7 +2040,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redis": {
"name": "redis",
@@ -2115,7 +2172,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
}
},
"enums": {
@@ -2220,5 +2280,9 @@
"columns": {}
},
"id": "3a4dfad7-ae33-4ae3-b60e-4f40f44f5652",
"prevId": "c6215051-7cd1-412d-b8df-b50d58acacff"
"prevId": "c6215051-7cd1-412d-b8df-b50d58acacff",
"sequences": {},
"policies": {},
"views": {},
"roles": {}
}

View File

@@ -1,5 +1,5 @@
{
"version": "6",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.application": {
@@ -252,7 +252,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.postgres": {
"name": "postgres",
@@ -393,7 +396,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.user": {
"name": "user",
@@ -522,7 +528,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.admin": {
"name": "admin",
@@ -638,7 +647,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.auth": {
"name": "auth",
@@ -705,7 +717,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.project": {
"name": "project",
@@ -759,7 +774,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.domain": {
"name": "domain",
@@ -841,7 +859,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mariadb": {
"name": "mariadb",
@@ -988,7 +1009,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mongo": {
"name": "mongo",
@@ -1123,7 +1147,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mysql": {
"name": "mysql",
@@ -1270,7 +1297,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.backup": {
"name": "backup",
@@ -1412,7 +1442,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.destination": {
"name": "destination",
@@ -1484,7 +1517,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.deployment": {
"name": "deployment",
@@ -1545,7 +1581,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mount": {
"name": "mount",
@@ -1713,7 +1752,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.certificate": {
"name": "certificate",
@@ -1767,7 +1809,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.session": {
"name": "session",
@@ -1809,7 +1854,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redirect": {
"name": "redirect",
@@ -1876,7 +1924,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.security": {
"name": "security",
@@ -1939,7 +1990,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.port": {
"name": "port",
@@ -1993,7 +2047,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redis": {
"name": "redis",
@@ -2122,7 +2179,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
}
},
"enums": {
@@ -2227,5 +2287,9 @@
"columns": {}
},
"id": "665483bd-5123-4c2b-beef-bfa9b91b9356",
"prevId": "3a4dfad7-ae33-4ae3-b60e-4f40f44f5652"
"prevId": "3a4dfad7-ae33-4ae3-b60e-4f40f44f5652",
"sequences": {},
"policies": {},
"views": {},
"roles": {}
}

View File

@@ -1,5 +1,5 @@
{
"version": "6",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.application": {
@@ -252,7 +252,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.postgres": {
"name": "postgres",
@@ -393,7 +396,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.user": {
"name": "user",
@@ -529,7 +535,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.admin": {
"name": "admin",
@@ -645,7 +654,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.auth": {
"name": "auth",
@@ -712,7 +724,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.project": {
"name": "project",
@@ -766,7 +781,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.domain": {
"name": "domain",
@@ -848,7 +866,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mariadb": {
"name": "mariadb",
@@ -995,7 +1016,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mongo": {
"name": "mongo",
@@ -1130,7 +1154,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mysql": {
"name": "mysql",
@@ -1277,7 +1304,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.backup": {
"name": "backup",
@@ -1419,7 +1449,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.destination": {
"name": "destination",
@@ -1491,7 +1524,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.deployment": {
"name": "deployment",
@@ -1552,7 +1588,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mount": {
"name": "mount",
@@ -1720,7 +1759,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.certificate": {
"name": "certificate",
@@ -1774,7 +1816,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.session": {
"name": "session",
@@ -1816,7 +1861,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redirect": {
"name": "redirect",
@@ -1883,7 +1931,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.security": {
"name": "security",
@@ -1946,7 +1997,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.port": {
"name": "port",
@@ -2000,7 +2054,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redis": {
"name": "redis",
@@ -2129,7 +2186,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
}
},
"enums": {
@@ -2234,5 +2294,9 @@
"columns": {}
},
"id": "5a1d3f2b-9c31-4125-9645-015170550b51",
"prevId": "665483bd-5123-4c2b-beef-bfa9b91b9356"
"prevId": "665483bd-5123-4c2b-beef-bfa9b91b9356",
"sequences": {},
"policies": {},
"views": {},
"roles": {}
}

View File

@@ -1,5 +1,5 @@
{
"version": "6",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.application": {
@@ -228,7 +228,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.postgres": {
"name": "postgres",
@@ -369,7 +372,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.user": {
"name": "user",
@@ -505,7 +511,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.admin": {
"name": "admin",
@@ -621,7 +630,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.auth": {
"name": "auth",
@@ -688,7 +700,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.project": {
"name": "project",
@@ -742,7 +757,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.domain": {
"name": "domain",
@@ -824,7 +842,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mariadb": {
"name": "mariadb",
@@ -971,7 +992,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mongo": {
"name": "mongo",
@@ -1106,7 +1130,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mysql": {
"name": "mysql",
@@ -1253,7 +1280,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.backup": {
"name": "backup",
@@ -1395,7 +1425,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.destination": {
"name": "destination",
@@ -1467,7 +1500,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.deployment": {
"name": "deployment",
@@ -1528,7 +1564,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mount": {
"name": "mount",
@@ -1696,7 +1735,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.certificate": {
"name": "certificate",
@@ -1750,7 +1792,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.session": {
"name": "session",
@@ -1792,7 +1837,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redirect": {
"name": "redirect",
@@ -1859,7 +1907,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.security": {
"name": "security",
@@ -1922,7 +1973,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.port": {
"name": "port",
@@ -1976,7 +2030,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redis": {
"name": "redis",
@@ -2105,7 +2162,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
}
},
"enums": {
@@ -2210,5 +2270,9 @@
"columns": {}
},
"id": "7bb4bbcf-791c-4888-919e-f74bc0528b5f",
"prevId": "5a1d3f2b-9c31-4125-9645-015170550b51"
"prevId": "5a1d3f2b-9c31-4125-9645-015170550b51",
"sequences": {},
"policies": {},
"views": {},
"roles": {}
}

View File

@@ -1,5 +1,5 @@
{
"version": "6",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.application": {
@@ -228,7 +228,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.postgres": {
"name": "postgres",
@@ -369,7 +372,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.user": {
"name": "user",
@@ -505,7 +511,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.admin": {
"name": "admin",
@@ -621,7 +630,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.auth": {
"name": "auth",
@@ -688,7 +700,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.project": {
"name": "project",
@@ -742,7 +757,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.domain": {
"name": "domain",
@@ -824,7 +842,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mariadb": {
"name": "mariadb",
@@ -971,7 +992,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mongo": {
"name": "mongo",
@@ -1106,7 +1130,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mysql": {
"name": "mysql",
@@ -1253,7 +1280,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.backup": {
"name": "backup",
@@ -1395,7 +1425,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.destination": {
"name": "destination",
@@ -1467,7 +1500,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.deployment": {
"name": "deployment",
@@ -1528,7 +1564,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mount": {
"name": "mount",
@@ -1696,7 +1735,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.certificate": {
"name": "certificate",
@@ -1750,7 +1792,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.session": {
"name": "session",
@@ -1792,7 +1837,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redirect": {
"name": "redirect",
@@ -1859,7 +1907,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.security": {
"name": "security",
@@ -1922,7 +1973,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.port": {
"name": "port",
@@ -1976,7 +2030,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redis": {
"name": "redis",
@@ -2105,7 +2162,10 @@
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.registry": {
"name": "registry",
@@ -2178,7 +2238,10 @@
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
}
},
"enums": {
@@ -2291,5 +2354,9 @@
"columns": {}
},
"id": "92c75e26-64ef-484f-a7d2-72a9422c119f",
"prevId": "7bb4bbcf-791c-4888-919e-f74bc0528b5f"
"prevId": "7bb4bbcf-791c-4888-919e-f74bc0528b5f",
"sequences": {},
"policies": {},
"views": {},
"roles": {}
}

View File

@@ -1,7 +1,5 @@
{
"id": "8ca71247-d512-427d-b115-47a7287ac431",
"prevId": "92c75e26-64ef-484f-a7d2-72a9422c119f",
"version": "6",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.application": {
@@ -219,40 +217,43 @@
"application_registryId_registry_registryId_fk": {
"name": "application_registryId_registry_registryId_fk",
"tableFrom": "application",
"tableTo": "registry",
"columnsFrom": [
"registryId"
],
"tableTo": "registry",
"columnsTo": [
"registryId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"application_projectId_project_projectId_fk": {
"name": "application_projectId_project_projectId_fk",
"tableFrom": "application",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"application_appName_unique": {
"name": "application_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.postgres": {
"name": "postgres",
@@ -374,27 +375,30 @@
"postgres_projectId_project_projectId_fk": {
"name": "postgres_projectId_project_projectId_fk",
"tableFrom": "postgres",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"postgres_appName_unique": {
"name": "postgres_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.user": {
"name": "user",
@@ -505,32 +509,35 @@
"user_adminId_admin_adminId_fk": {
"name": "user_adminId_admin_adminId_fk",
"tableFrom": "user",
"tableTo": "admin",
"columnsFrom": [
"adminId"
],
"tableTo": "admin",
"columnsTo": [
"adminId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"user_authId_auth_id_fk": {
"name": "user_authId_auth_id_fk",
"tableFrom": "user",
"tableTo": "auth",
"columnsFrom": [
"authId"
],
"tableTo": "auth",
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.admin": {
"name": "admin",
@@ -635,19 +642,22 @@
"admin_authId_auth_id_fk": {
"name": "admin_authId_auth_id_fk",
"tableFrom": "admin",
"tableTo": "auth",
"columnsFrom": [
"authId"
],
"tableTo": "auth",
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.auth": {
"name": "auth",
@@ -710,12 +720,15 @@
"uniqueConstraints": {
"auth_email_unique": {
"name": "auth_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.project": {
"name": "project",
@@ -757,19 +770,22 @@
"project_adminId_admin_adminId_fk": {
"name": "project_adminId_admin_adminId_fk",
"tableFrom": "project",
"tableTo": "admin",
"columnsFrom": [
"adminId"
],
"tableTo": "admin",
"columnsTo": [
"adminId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.domain": {
"name": "domain",
@@ -840,19 +856,22 @@
"domain_applicationId_application_applicationId_fk": {
"name": "domain_applicationId_application_applicationId_fk",
"tableFrom": "domain",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mariadb": {
"name": "mariadb",
@@ -980,27 +999,30 @@
"mariadb_projectId_project_projectId_fk": {
"name": "mariadb_projectId_project_projectId_fk",
"tableFrom": "mariadb",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"mariadb_appName_unique": {
"name": "mariadb_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mongo": {
"name": "mongo",
@@ -1116,27 +1138,30 @@
"mongo_projectId_project_projectId_fk": {
"name": "mongo_projectId_project_projectId_fk",
"tableFrom": "mongo",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"mongo_appName_unique": {
"name": "mongo_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mysql": {
"name": "mysql",
@@ -1264,27 +1289,30 @@
"mysql_projectId_project_projectId_fk": {
"name": "mysql_projectId_project_projectId_fk",
"tableFrom": "mysql",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"mysql_appName_unique": {
"name": "mysql_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.backup": {
"name": "backup",
@@ -1363,71 +1391,74 @@
"backup_destinationId_destination_destinationId_fk": {
"name": "backup_destinationId_destination_destinationId_fk",
"tableFrom": "backup",
"tableTo": "destination",
"columnsFrom": [
"destinationId"
],
"tableTo": "destination",
"columnsTo": [
"destinationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_postgresId_postgres_postgresId_fk": {
"name": "backup_postgresId_postgres_postgresId_fk",
"tableFrom": "backup",
"tableTo": "postgres",
"columnsFrom": [
"postgresId"
],
"tableTo": "postgres",
"columnsTo": [
"postgresId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_mariadbId_mariadb_mariadbId_fk": {
"name": "backup_mariadbId_mariadb_mariadbId_fk",
"tableFrom": "backup",
"tableTo": "mariadb",
"columnsFrom": [
"mariadbId"
],
"tableTo": "mariadb",
"columnsTo": [
"mariadbId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_mysqlId_mysql_mysqlId_fk": {
"name": "backup_mysqlId_mysql_mysqlId_fk",
"tableFrom": "backup",
"tableTo": "mysql",
"columnsFrom": [
"mysqlId"
],
"tableTo": "mysql",
"columnsTo": [
"mysqlId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_mongoId_mongo_mongoId_fk": {
"name": "backup_mongoId_mongo_mongoId_fk",
"tableFrom": "backup",
"tableTo": "mongo",
"columnsFrom": [
"mongoId"
],
"tableTo": "mongo",
"columnsTo": [
"mongoId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.destination": {
"name": "destination",
@@ -1487,19 +1518,22 @@
"destination_adminId_admin_adminId_fk": {
"name": "destination_adminId_admin_adminId_fk",
"tableFrom": "destination",
"tableTo": "admin",
"columnsFrom": [
"adminId"
],
"tableTo": "admin",
"columnsTo": [
"adminId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.deployment": {
"name": "deployment",
@@ -1549,19 +1583,22 @@
"deployment_applicationId_application_applicationId_fk": {
"name": "deployment_applicationId_application_applicationId_fk",
"tableFrom": "deployment",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mount": {
"name": "mount",
@@ -1654,84 +1691,87 @@
"mount_applicationId_application_applicationId_fk": {
"name": "mount_applicationId_application_applicationId_fk",
"tableFrom": "mount",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_postgresId_postgres_postgresId_fk": {
"name": "mount_postgresId_postgres_postgresId_fk",
"tableFrom": "mount",
"tableTo": "postgres",
"columnsFrom": [
"postgresId"
],
"tableTo": "postgres",
"columnsTo": [
"postgresId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_mariadbId_mariadb_mariadbId_fk": {
"name": "mount_mariadbId_mariadb_mariadbId_fk",
"tableFrom": "mount",
"tableTo": "mariadb",
"columnsFrom": [
"mariadbId"
],
"tableTo": "mariadb",
"columnsTo": [
"mariadbId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_mongoId_mongo_mongoId_fk": {
"name": "mount_mongoId_mongo_mongoId_fk",
"tableFrom": "mount",
"tableTo": "mongo",
"columnsFrom": [
"mongoId"
],
"tableTo": "mongo",
"columnsTo": [
"mongoId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_mysqlId_mysql_mysqlId_fk": {
"name": "mount_mysqlId_mysql_mysqlId_fk",
"tableFrom": "mount",
"tableTo": "mysql",
"columnsFrom": [
"mysqlId"
],
"tableTo": "mysql",
"columnsTo": [
"mysqlId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_redisId_redis_redisId_fk": {
"name": "mount_redisId_redis_redisId_fk",
"tableFrom": "mount",
"tableTo": "redis",
"columnsFrom": [
"redisId"
],
"tableTo": "redis",
"columnsTo": [
"redisId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.certificate": {
"name": "certificate",
@@ -1780,12 +1820,15 @@
"uniqueConstraints": {
"certificate_certificatePath_unique": {
"name": "certificate_certificatePath_unique",
"nullsNotDistinct": false,
"columns": [
"certificatePath"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.session": {
"name": "session",
@@ -1815,19 +1858,22 @@
"session_user_id_auth_id_fk": {
"name": "session_user_id_auth_id_fk",
"tableFrom": "session",
"tableTo": "auth",
"columnsFrom": [
"user_id"
],
"tableTo": "auth",
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redirect": {
"name": "redirect",
@@ -1882,19 +1928,22 @@
"redirect_applicationId_application_applicationId_fk": {
"name": "redirect_applicationId_application_applicationId_fk",
"tableFrom": "redirect",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.security": {
"name": "security",
@@ -1936,28 +1985,31 @@
"security_applicationId_application_applicationId_fk": {
"name": "security_applicationId_application_applicationId_fk",
"tableFrom": "security",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"security_username_applicationId_unique": {
"name": "security_username_applicationId_unique",
"nullsNotDistinct": false,
"columns": [
"username",
"applicationId"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.port": {
"name": "port",
@@ -2000,19 +2052,22 @@
"port_applicationId_application_applicationId_fk": {
"name": "port_applicationId_application_applicationId_fk",
"tableFrom": "port",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redis": {
"name": "redis",
@@ -2122,27 +2177,30 @@
"redis_projectId_project_projectId_fk": {
"name": "redis_projectId_project_projectId_fk",
"tableFrom": "redis",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"redis_appName_unique": {
"name": "redis_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.registry": {
"name": "registry",
@@ -2204,19 +2262,22 @@
"registry_adminId_admin_adminId_fk": {
"name": "registry_adminId_admin_adminId_fk",
"tableFrom": "registry",
"tableTo": "admin",
"columnsFrom": [
"adminId"
],
"tableTo": "admin",
"columnsTo": [
"adminId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
}
},
"enums": {
@@ -2324,8 +2385,14 @@
},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
"tables": {},
"columns": {}
},
"id": "8ca71247-d512-427d-b115-47a7287ac431",
"prevId": "92c75e26-64ef-484f-a7d2-72a9422c119f",
"sequences": {},
"policies": {},
"views": {},
"roles": {}
}

View File

@@ -1,7 +1,5 @@
{
"id": "80176730-273b-45e4-a7ca-760e29ad6d02",
"prevId": "8ca71247-d512-427d-b115-47a7287ac431",
"version": "6",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.application": {
@@ -226,40 +224,43 @@
"application_registryId_registry_registryId_fk": {
"name": "application_registryId_registry_registryId_fk",
"tableFrom": "application",
"tableTo": "registry",
"columnsFrom": [
"registryId"
],
"tableTo": "registry",
"columnsTo": [
"registryId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"application_projectId_project_projectId_fk": {
"name": "application_projectId_project_projectId_fk",
"tableFrom": "application",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"application_appName_unique": {
"name": "application_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.postgres": {
"name": "postgres",
@@ -381,27 +382,30 @@
"postgres_projectId_project_projectId_fk": {
"name": "postgres_projectId_project_projectId_fk",
"tableFrom": "postgres",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"postgres_appName_unique": {
"name": "postgres_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.user": {
"name": "user",
@@ -512,32 +516,35 @@
"user_adminId_admin_adminId_fk": {
"name": "user_adminId_admin_adminId_fk",
"tableFrom": "user",
"tableTo": "admin",
"columnsFrom": [
"adminId"
],
"tableTo": "admin",
"columnsTo": [
"adminId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"user_authId_auth_id_fk": {
"name": "user_authId_auth_id_fk",
"tableFrom": "user",
"tableTo": "auth",
"columnsFrom": [
"authId"
],
"tableTo": "auth",
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.admin": {
"name": "admin",
@@ -642,19 +649,22 @@
"admin_authId_auth_id_fk": {
"name": "admin_authId_auth_id_fk",
"tableFrom": "admin",
"tableTo": "auth",
"columnsFrom": [
"authId"
],
"tableTo": "auth",
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.auth": {
"name": "auth",
@@ -717,12 +727,15 @@
"uniqueConstraints": {
"auth_email_unique": {
"name": "auth_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.project": {
"name": "project",
@@ -764,19 +777,22 @@
"project_adminId_admin_adminId_fk": {
"name": "project_adminId_admin_adminId_fk",
"tableFrom": "project",
"tableTo": "admin",
"columnsFrom": [
"adminId"
],
"tableTo": "admin",
"columnsTo": [
"adminId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.domain": {
"name": "domain",
@@ -847,19 +863,22 @@
"domain_applicationId_application_applicationId_fk": {
"name": "domain_applicationId_application_applicationId_fk",
"tableFrom": "domain",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mariadb": {
"name": "mariadb",
@@ -987,27 +1006,30 @@
"mariadb_projectId_project_projectId_fk": {
"name": "mariadb_projectId_project_projectId_fk",
"tableFrom": "mariadb",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"mariadb_appName_unique": {
"name": "mariadb_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mongo": {
"name": "mongo",
@@ -1123,27 +1145,30 @@
"mongo_projectId_project_projectId_fk": {
"name": "mongo_projectId_project_projectId_fk",
"tableFrom": "mongo",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"mongo_appName_unique": {
"name": "mongo_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mysql": {
"name": "mysql",
@@ -1271,27 +1296,30 @@
"mysql_projectId_project_projectId_fk": {
"name": "mysql_projectId_project_projectId_fk",
"tableFrom": "mysql",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"mysql_appName_unique": {
"name": "mysql_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.backup": {
"name": "backup",
@@ -1370,71 +1398,74 @@
"backup_destinationId_destination_destinationId_fk": {
"name": "backup_destinationId_destination_destinationId_fk",
"tableFrom": "backup",
"tableTo": "destination",
"columnsFrom": [
"destinationId"
],
"tableTo": "destination",
"columnsTo": [
"destinationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_postgresId_postgres_postgresId_fk": {
"name": "backup_postgresId_postgres_postgresId_fk",
"tableFrom": "backup",
"tableTo": "postgres",
"columnsFrom": [
"postgresId"
],
"tableTo": "postgres",
"columnsTo": [
"postgresId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_mariadbId_mariadb_mariadbId_fk": {
"name": "backup_mariadbId_mariadb_mariadbId_fk",
"tableFrom": "backup",
"tableTo": "mariadb",
"columnsFrom": [
"mariadbId"
],
"tableTo": "mariadb",
"columnsTo": [
"mariadbId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_mysqlId_mysql_mysqlId_fk": {
"name": "backup_mysqlId_mysql_mysqlId_fk",
"tableFrom": "backup",
"tableTo": "mysql",
"columnsFrom": [
"mysqlId"
],
"tableTo": "mysql",
"columnsTo": [
"mysqlId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"backup_mongoId_mongo_mongoId_fk": {
"name": "backup_mongoId_mongo_mongoId_fk",
"tableFrom": "backup",
"tableTo": "mongo",
"columnsFrom": [
"mongoId"
],
"tableTo": "mongo",
"columnsTo": [
"mongoId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.destination": {
"name": "destination",
@@ -1494,19 +1525,22 @@
"destination_adminId_admin_adminId_fk": {
"name": "destination_adminId_admin_adminId_fk",
"tableFrom": "destination",
"tableTo": "admin",
"columnsFrom": [
"adminId"
],
"tableTo": "admin",
"columnsTo": [
"adminId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.deployment": {
"name": "deployment",
@@ -1556,19 +1590,22 @@
"deployment_applicationId_application_applicationId_fk": {
"name": "deployment_applicationId_application_applicationId_fk",
"tableFrom": "deployment",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.mount": {
"name": "mount",
@@ -1661,84 +1698,87 @@
"mount_applicationId_application_applicationId_fk": {
"name": "mount_applicationId_application_applicationId_fk",
"tableFrom": "mount",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_postgresId_postgres_postgresId_fk": {
"name": "mount_postgresId_postgres_postgresId_fk",
"tableFrom": "mount",
"tableTo": "postgres",
"columnsFrom": [
"postgresId"
],
"tableTo": "postgres",
"columnsTo": [
"postgresId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_mariadbId_mariadb_mariadbId_fk": {
"name": "mount_mariadbId_mariadb_mariadbId_fk",
"tableFrom": "mount",
"tableTo": "mariadb",
"columnsFrom": [
"mariadbId"
],
"tableTo": "mariadb",
"columnsTo": [
"mariadbId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_mongoId_mongo_mongoId_fk": {
"name": "mount_mongoId_mongo_mongoId_fk",
"tableFrom": "mount",
"tableTo": "mongo",
"columnsFrom": [
"mongoId"
],
"tableTo": "mongo",
"columnsTo": [
"mongoId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_mysqlId_mysql_mysqlId_fk": {
"name": "mount_mysqlId_mysql_mysqlId_fk",
"tableFrom": "mount",
"tableTo": "mysql",
"columnsFrom": [
"mysqlId"
],
"tableTo": "mysql",
"columnsTo": [
"mysqlId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
},
"mount_redisId_redis_redisId_fk": {
"name": "mount_redisId_redis_redisId_fk",
"tableFrom": "mount",
"tableTo": "redis",
"columnsFrom": [
"redisId"
],
"tableTo": "redis",
"columnsTo": [
"redisId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.certificate": {
"name": "certificate",
@@ -1787,12 +1827,15 @@
"uniqueConstraints": {
"certificate_certificatePath_unique": {
"name": "certificate_certificatePath_unique",
"nullsNotDistinct": false,
"columns": [
"certificatePath"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.session": {
"name": "session",
@@ -1822,19 +1865,22 @@
"session_user_id_auth_id_fk": {
"name": "session_user_id_auth_id_fk",
"tableFrom": "session",
"tableTo": "auth",
"columnsFrom": [
"user_id"
],
"tableTo": "auth",
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redirect": {
"name": "redirect",
@@ -1889,19 +1935,22 @@
"redirect_applicationId_application_applicationId_fk": {
"name": "redirect_applicationId_application_applicationId_fk",
"tableFrom": "redirect",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.security": {
"name": "security",
@@ -1943,28 +1992,31 @@
"security_applicationId_application_applicationId_fk": {
"name": "security_applicationId_application_applicationId_fk",
"tableFrom": "security",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"security_username_applicationId_unique": {
"name": "security_username_applicationId_unique",
"nullsNotDistinct": false,
"columns": [
"username",
"applicationId"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.port": {
"name": "port",
@@ -2007,19 +2059,22 @@
"port_applicationId_application_applicationId_fk": {
"name": "port_applicationId_application_applicationId_fk",
"tableFrom": "port",
"tableTo": "application",
"columnsFrom": [
"applicationId"
],
"tableTo": "application",
"columnsTo": [
"applicationId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.redis": {
"name": "redis",
@@ -2129,27 +2184,30 @@
"redis_projectId_project_projectId_fk": {
"name": "redis_projectId_project_projectId_fk",
"tableFrom": "redis",
"tableTo": "project",
"columnsFrom": [
"projectId"
],
"tableTo": "project",
"columnsTo": [
"projectId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"redis_appName_unique": {
"name": "redis_appName_unique",
"nullsNotDistinct": false,
"columns": [
"appName"
]
],
"nullsNotDistinct": false
}
}
},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
},
"public.registry": {
"name": "registry",
@@ -2211,19 +2269,22 @@
"registry_adminId_admin_adminId_fk": {
"name": "registry_adminId_admin_adminId_fk",
"tableFrom": "registry",
"tableTo": "admin",
"columnsFrom": [
"adminId"
],
"tableTo": "admin",
"columnsTo": [
"adminId"
],
"onDelete": "cascade",
"onUpdate": "no action"
"onUpdate": "no action",
"onDelete": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
"uniqueConstraints": {},
"policies": {},
"isRLSEnabled": false,
"checkConstraints": {}
}
},
"enums": {
@@ -2331,8 +2392,14 @@
},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
"tables": {},
"columns": {}
},
"id": "80176730-273b-45e4-a7ca-760e29ad6d02",
"prevId": "8ca71247-d512-427d-b115-47a7287ac431",
"sequences": {},
"policies": {},
"views": {},
"roles": {}
}

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