Compare commits

..

160 Commits

Author SHA1 Message Date
Mauricio Siu
790894ab93 refactor: migrate admin API calls to user router 2025-02-20 23:02:02 -06:00
Mauricio Siu
5a1145996d feat: add backup code authentication for 2FA login 2025-02-20 01:50:01 -06:00
Mauricio Siu
a9e12c2b18 refactor: update organization context in API routers 2025-02-20 01:42:35 -06:00
Mauricio Siu
b73e4102dd feat: add organizations and members 2025-02-17 02:48:42 -06:00
Mauricio Siu
c7d47a6003 refactor: update database foreign key constraints and user management 2025-02-17 00:30:15 -06:00
Mauricio Siu
8c28223343 refactor: remove 2fa migration 2025-02-17 00:10:34 -06:00
Mauricio Siu
7abe060fcf feat: enhance two-factor authentication and auth client implementation 2025-02-17 00:07:36 -06:00
Mauricio Siu
0e8e92c715 refactor: add 2fa 2025-02-16 20:56:50 -06:00
Mauricio Siu
e1632cbdb3 refactor: update user and authentication schema with two-factor support 2025-02-16 15:32:57 -06:00
Mauricio Siu
90156da570 refactor: remove tables 2025-02-16 14:11:47 -06:00
Mauricio Siu
9856502ece refactor: remove old references 2025-02-16 13:55:27 -06:00
Mauricio Siu
a8d1471b16 refactor: update 2025-02-16 13:28:29 -06:00
Mauricio Siu
27736c7c97 refactor: update role and validation handling across multiple pages 2025-02-16 03:06:22 -06:00
Mauricio Siu
e7db0ccb70 refactor: update invitation 2025-02-16 02:57:49 -06:00
Mauricio Siu
4a1a14aeb4 refactor: update 2025-02-15 23:24:45 -06:00
Mauricio Siu
ed62b4e1a3 refactor: lint 2025-02-15 23:01:44 -06:00
Mauricio Siu
515d65d993 refactor: adjust queries 2025-02-15 23:01:36 -06:00
Mauricio Siu
78c72b6337 refactor: update 2025-02-15 20:49:10 -06:00
Mauricio Siu
e3e35ce792 refactor: update to use organization resources 2025-02-15 20:43:23 -06:00
Mauricio Siu
6d0e195a4d refactor: update 2025-02-15 20:26:05 -06:00
Mauricio Siu
53ce5e57fa refactor: update organization 2025-02-15 20:25:58 -06:00
Mauricio Siu
87b12ff6e9 refactor: update 2025-02-15 20:06:33 -06:00
Mauricio Siu
8b71f963cc refactor: update logic 2025-02-15 19:35:22 -06:00
Mauricio Siu
1c5cc5a0db refactor: update roles 2025-02-15 19:23:08 -06:00
Mauricio Siu
d233f2c764 feat: adjust roles 2025-02-15 19:12:44 -06:00
Mauricio Siu
1bbb4c9b64 refactor: update migration 2025-02-15 18:13:20 -06:00
Mauricio Siu
6ec60b6bab refactor: update validation 2025-02-15 13:14:48 -06:00
Mauricio Siu
55abac3f2f refactor: migrate endpoints 2025-02-14 02:52:37 -06:00
Mauricio Siu
b6c29ccf05 refactor: update 2025-02-14 02:40:11 -06:00
Mauricio Siu
ca217affe6 feat: update references 2025-02-14 02:18:53 -06:00
Mauricio Siu
5c24281f72 refactor: return correct information 2025-02-13 02:45:33 -06:00
Mauricio Siu
bc901bcb25 refactor: update 2025-02-13 02:36:08 -06:00
Mauricio Siu
7c0d223e17 refactor: add fields 2025-02-13 01:42:58 -06:00
Mauricio Siu
74ee024cf9 refactor: update temps 2025-02-13 01:24:25 -06:00
Mauricio Siu
140a871275 refactor: update 2025-02-13 01:21:49 -06:00
Mauricio Siu
d1f72a2e20 refactor: update migration 2025-02-13 00:57:22 -06:00
Mauricio Siu
0d525398a8 feat: migrate rest schemas 2025-02-13 00:45:29 -06:00
Mauricio Siu
7c62408070 refactor: delete 2025-02-13 00:38:39 -06:00
Mauricio Siu
23f1ce17de refactor: add migration 2025-02-13 00:38:22 -06:00
Mauricio Siu
60eee55f2d refactor: test migrastion 2025-02-12 23:41:04 -06:00
Mauricio Siu
8f562eefc1 Merge branch 'canary' into feat/better-auth 2025-02-12 20:56:23 -06:00
Mauricio Siu
6179cef1ee refactor: update name 2025-02-10 02:13:52 -06:00
Mauricio Siu
b7112b89fd refactor: add migration 2025-02-10 00:39:46 -06:00
Mauricio Siu
030c8a312d Update package.json 2025-02-10 00:24:58 -06:00
Mauricio Siu
1db6ba94f4 refactor: remove 2025-02-09 21:36:36 -06:00
Mauricio Siu
afd3d2eea3 refactor: lint 2025-02-09 20:53:14 -06:00
Mauricio Siu
8bd72a8a34 refactor: add organizations system 2025-02-09 20:53:06 -06:00
Mauricio Siu
fafc238e70 refactor: migration 2025-02-09 18:56:17 -06:00
Mauricio Siu
c04bf3c7e0 feat: add migration 2025-02-09 18:19:21 -06:00
Mauricio Siu
6b9fd596e5 feat: add openalternative 2025-02-09 03:17:13 -06:00
Mauricio Siu
7e36433144 Merge pull request #1282 from wish-oss/feat/bulk-actions
feat: added bulk actions for services start and stop and added service status for domain dropdown
2025-02-09 03:07:01 -06:00
Mauricio Siu
0a6554c275 refactor: add loading action 2025-02-09 03:06:18 -06:00
Mauricio Siu
fcc55355f2 refactor: add catch to prevent throw error 2025-02-09 03:02:39 -06:00
Mauricio Siu
78e606876a Merge pull request #1297 from mohabgabber/canary
Update unsend version to v1.3.2
2025-02-09 02:37:31 -06:00
Mauricio Siu
7e99baa267 Merge branch 'canary' into canary 2025-02-09 02:37:23 -06:00
Mauricio Siu
92c03bb7cc Merge pull request #1276 from Dokploy/1004-network-conflict
1004 network conflict
2025-02-09 02:36:17 -06:00
Mauricio Siu
3a5ecb2f64 refactor: remove unused imports 2025-02-09 02:33:30 -06:00
Mauricio Siu
c0a00f4957 refactor: remove dokploy-network 2025-02-09 02:31:01 -06:00
Mauricio Siu
a8f94540f9 refactor: lint 2025-02-09 02:20:40 -06:00
Mauricio Siu
3e2cfe6eb8 refactor: agroupate utilities 2025-02-09 02:20:28 -06:00
Mohab Gabber
b2d5090b36 Merge branch 'canary' of https://github.com/mohabgabber/dokploy into canary 2025-02-09 03:22:27 +02:00
Mohab Gabber
0a0f53e9de chore: update unsend version to v1.3.2 2025-02-09 03:22:23 +02:00
Vishal kadam
17ce03e529 Merge branch 'Dokploy:canary' into feat/bulk-actions 2025-02-09 01:47:55 +05:30
Mauricio Siu
f44512a437 refactor: add condition to deploy on remote servers 2025-02-06 01:52:53 -06:00
Mauricio Siu
8379068fe3 refactor: remove services 2025-02-06 00:40:03 -06:00
Mauricio Siu
a71de72a3c refactor: remove services 2025-02-06 00:39:42 -06:00
Mauricio Siu
b024060eed refactor: delete unneeded container_name 2025-02-06 00:38:04 -06:00
Mauricio Siu
56b26ce0d5 refactor: use appname in network connect 2025-02-06 00:19:34 -06:00
Mauricio Siu
a9e3a65782 Merge branch 'canary' into 1004-network-conflict 2025-02-06 00:17:26 -06:00
Mauricio Siu
7a472df753 Merge pull request #1239 from NagariaHussain/template-frappe-hr
feat(template): frappe HR, open source HR & Payroll software
2025-02-06 00:14:59 -06:00
vishalkadam47
bd809c8dca feat: added bulk actions for services start and stop and added service status for domain dropdown 2025-02-05 08:17:15 +05:30
Hussain Nagaria
48642979c5 chore: make erpnext template more configurable 2025-02-04 17:17:43 +05:30
Hussain Nagaria
46411a5f4e fix: create site should use configured db 2025-02-04 14:30:55 +05:30
Hussain Nagaria
82cf0643d7 fix: site volume configurable 2025-02-04 14:15:47 +05:30
Hussain Nagaria
65780ee852 feat: make db configurable 2025-02-04 13:57:49 +05:30
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
d424524d69 refactor: lint 2025-02-03 00:57:27 -06:00
Mauricio Siu
6f2148c060 feat: add deployable option to randomize and prevent colission in duplicate templates 2025-02-03 00:57:18 -06: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
79fca72d06 Merge branch 'canary' into template-frappe-hr 2025-01-30 23:32:56 -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
Hussain Nagaria
62a3707c10 feat(template): frappe HR, open source HR & Payroll software 2025-01-29 18:49:27 +05:30
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
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
João Gabriel
a46cbf4f2c feat: evolution api template 2025-01-14 08:36:29 -03: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
406 changed files with 105868 additions and 15141 deletions

BIN
.github/sponsors/openalternative.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

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

View File

@@ -2,7 +2,7 @@ name: Dokploy Docker Build
on:
push:
branches: [main, canary, feat/github-runners]
branches: [main, canary, "feat/monitoring"]
env:
IMAGE_NAME: dokploy/dokploy

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

@@ -93,6 +93,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<a href="https://cloudblast.io/?ref=dokploy "><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
</div>
### Community Backers 🤝

View File

@@ -45,7 +45,7 @@ const baseApp: ApplicationNested = {
previewWildcard: "",
project: {
env: "",
adminId: "",
organizationId: "",
name: "",
description: "",
createdAt: "",

View File

@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
default: fs,
}));
import type { Admin, FileConfig } from "@dokploy/server";
import type { FileConfig, User } from "@dokploy/server";
import {
createDefaultServerTraefikConfig,
loadOrCreateConfig,
@@ -13,13 +13,34 @@ import {
} from "@dokploy/server";
import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: Admin = {
const baseAdmin: User = {
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",
createdAt: new Date(),
serverIp: null,
certificateType: "none",
host: null,
@@ -30,6 +51,31 @@ const baseAdmin: Admin = {
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
accessedProjects: [],
accessedServices: [],
banExpires: new Date(),
banned: true,
banReason: "",
canAccessToAPI: false,
canCreateProjects: false,
canDeleteProjects: false,
canDeleteServices: false,
canAccessToDocker: false,
canAccessToSSHKeys: false,
canCreateServices: false,
canAccessToTraefikFiles: false,
canAccessToGitProviders: false,
email: "",
expirationDate: "",
id: "",
isRegistered: false,
name: "",
createdAt2: new Date().toISOString(),
emailVerified: false,
image: "",
token: "",
updatedAt: new Date(),
twoFactorEnabled: false,
};
beforeEach(() => {

View File

@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
previewWildcard: "",
project: {
env: "",
adminId: "",
organizationId: "",
name: "",
description: "",
createdAt: "",

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,64 +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"
size="icon"
className="group hover:bg-blue-500/10 "
<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}
>
<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((data) => {
refetch();
toast.success("Domain deleted successfully");
<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,
})
.catch(() => {
toast.error("Error deleting domain");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
.then(() => {
refetch();
toast.success("Domain deleted successfully");
})
.catch(() => {
toast.error("Error deleting domain");
});
}}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<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

@@ -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,63 +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"
size="icon"
className="group hover:bg-blue-500/10 "
<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}
>
<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");
<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,
})
.catch(() => {
toast.error("Error deleting domain");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
.then((data) => {
refetch();
toast.success("Domain deleted successfully");
})
.catch(() => {
toast.error("Error deleting domain");
});
}}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<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

@@ -14,7 +14,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
import { RandomizeCompose } from "./randomize-compose";
import { ShowUtilities } from "./show-utilities";
interface Props {
composeId: string;
@@ -125,7 +125,7 @@ services:
</Form>
<div className="flex justify-between flex-col lg:flex-row gap-2">
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
<RandomizeCompose composeId={composeId} />
<ShowUtilities composeId={composeId} />
</div>
<Button
type="submit"

View File

@@ -0,0 +1,191 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props {
composeId: string;
}
const schema = z.object({
isolatedDeployment: z.boolean().optional(),
});
type Schema = z.infer<typeof schema>;
export const IsolatedDeployment = ({ composeId }: Props) => {
const utils = api.useUtils();
const [compose, setCompose] = useState<string>("");
const { mutateAsync, error, isError } =
api.compose.isolatedDeployment.useMutation();
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
const { data, refetch } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
console.log(data);
const form = useForm<Schema>({
defaultValues: {
isolatedDeployment: false,
},
resolver: zodResolver(schema),
});
useEffect(() => {
randomizeCompose();
if (data) {
form.reset({
isolatedDeployment: data?.isolatedDeployment || false,
});
}
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (formData: Schema) => {
await updateCompose({
composeId,
isolatedDeployment: formData?.isolatedDeployment || false,
})
.then(async (data) => {
randomizeCompose();
refetch();
toast.success("Compose updated");
})
.catch(() => {
toast.error("Error updating the compose");
});
};
const randomizeCompose = async () => {
await mutateAsync({
composeId,
suffix: data?.appName || "",
})
.then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
toast.success("Compose Isolated");
})
.catch(() => {
toast.error("Error isolating the compose");
});
};
return (
<>
<DialogHeader>
<DialogTitle>Isolate Deployment</DialogTitle>
<DialogDescription>
Use this option to isolate the deployment of this compose file.
</DialogDescription>
</DialogHeader>
<div className="text-sm text-muted-foreground flex flex-col gap-2">
<span>
This feature creates an isolated environment for your deployment by
adding unique prefixes to all resources. It establishes a dedicated
network based on your compose file's name, ensuring your services run
in isolation. This prevents conflicts when running multiple instances
of the same template or services with identical names.
</span>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2">
Resources that will be isolated:
</h4>
<ul className="list-disc list-inside">
<li>Docker volumes</li>
<li>Docker networks</li>
</ul>
</div>
</div>
</div>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-add-project"
className="grid w-full gap-4"
>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<div className="flex flex-col lg:flex-col gap-4 w-full ">
<div>
<FormField
control={form.control}
name="isolatedDeployment"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Enable Randomize ({data?.appName})</FormLabel>
<FormDescription>
Enable randomize to the compose file.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
<Button
form="hook-form-add-project"
type="submit"
className="lg:w-fit"
>
Save
</Button>
</div>
</div>
<div className="flex flex-col gap-4">
<Label>Preview</Label>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</div>
</form>
</Form>
</>
);
};

View File

@@ -1,14 +1,10 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import { CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
@@ -20,11 +16,6 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -70,6 +61,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
const suffix = form.watch("suffix");
useEffect(() => {
randomizeCompose();
if (data) {
form.reset({
suffix: data?.suffix || "",
@@ -110,126 +102,117 @@ export const RandomizeCompose = ({ composeId }: Props) => {
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild onClick={() => randomizeCompose()}>
<Button className="max-lg:w-full" variant="outline">
<Dices className="h-4 w-4" />
Randomize Compose
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
<DialogHeader>
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
<DialogDescription>
Use this in case you want to deploy the same compose file and you
have conflicts with some property like volumes, networks, etc.
</DialogDescription>
</DialogHeader>
<div className="text-sm text-muted-foreground flex flex-col gap-2">
<span>
This will randomize the compose file and will add a suffix to the
property to avoid conflicts
</span>
<ul className="list-disc list-inside">
<li>volumes</li>
<li>networks</li>
<li>services</li>
<li>configs</li>
<li>secrets</li>
</ul>
<AlertBlock type="info">
When you activate this option, we will include a env
`COMPOSE_PREFIX` variable to the compose file so you can use it in
your compose file.
</AlertBlock>
</div>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-add-project"
className="grid w-full gap-4"
>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<div className="flex flex-col lg:flex-col gap-4 w-full ">
<div>
<FormField
control={form.control}
name="suffix"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center w-full">
<FormLabel>Suffix</FormLabel>
<FormControl>
<Input
placeholder="Enter a suffix (Optional, example: prod)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="randomize"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Apply Randomize</FormLabel>
<FormDescription>
Apply randomize to the compose file.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
<Button
form="hook-form-add-project"
type="submit"
className="lg:w-fit"
>
Save
</Button>
<Button
type="button"
variant="secondary"
onClick={async () => {
await randomizeCompose();
}}
className="lg:w-fit"
>
Random
</Button>
</div>
<div className="w-full">
<DialogHeader>
<DialogTitle>Randomize Compose (Experimental)</DialogTitle>
<DialogDescription>
Use this in case you want to deploy the same compose file and you have
conflicts with some property like volumes, networks, etc.
</DialogDescription>
</DialogHeader>
<div className="text-sm text-muted-foreground flex flex-col gap-2">
<span>
This will randomize the compose file and will add a suffix to the
property to avoid conflicts
</span>
<ul className="list-disc list-inside">
<li>volumes</li>
<li>networks</li>
<li>services</li>
<li>configs</li>
<li>secrets</li>
</ul>
<AlertBlock type="info">
When you activate this option, we will include a env `COMPOSE_PREFIX`
variable to the compose file so you can use it in your compose file.
</AlertBlock>
</div>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-add-project"
className="grid w-full gap-4"
>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
)}
<div className="flex flex-col lg:flex-col gap-4 w-full ">
<div>
<FormField
control={form.control}
name="suffix"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center w-full mt-4">
<FormLabel>Suffix</FormLabel>
<FormControl>
<Input
placeholder="Enter a suffix (Optional, example: prod)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</pre>
</form>
</Form>
</DialogContent>
</Dialog>
<FormField
control={form.control}
name="randomize"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Apply Randomize</FormLabel>
<FormDescription>
Apply randomize to the compose file.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
<Button
form="hook-form-add-project"
type="submit"
className="lg:w-fit"
>
Save
</Button>
<Button
type="button"
variant="secondary"
onClick={async () => {
await randomizeCompose();
}}
className="lg:w-fit"
>
Random
</Button>
</div>
</div>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</form>
</Form>
</div>
);
};

View File

@@ -0,0 +1,46 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useState } from "react";
import { IsolatedDeployment } from "./isolated-deployment";
import { RandomizeCompose } from "./randomize-compose";
interface Props {
composeId: string;
}
export const ShowUtilities = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">Show Utilities</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
<DialogHeader>
<DialogTitle>Utilities </DialogTitle>
<DialogDescription>Modify the application data</DialogDescription>
</DialogHeader>
<Tabs defaultValue="isolated">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="isolated">Isolated Deployment</TabsTrigger>
<TabsTrigger value="randomize">Randomize Compose</TabsTrigger>
</TabsList>
<TabsContent value="randomize" className="pt-5">
<RandomizeCompose composeId={composeId} />
</TabsContent>
<TabsContent value="isolated" className="pt-5">
<IsolatedDeployment composeId={composeId} />
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
};

View File

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

@@ -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 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.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.user.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.user.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="w-full">
<DockerMonitoring appName="dokploy" />
</div>
);
};

View File

@@ -0,0 +1,119 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { PenBoxIcon, Plus, SquarePen } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
interface Props {
organizationId?: string;
children?: React.ReactNode;
}
export function AddOrganization({ organizationId, children }: Props) {
const utils = api.useUtils();
const { data: organization } = api.organization.one.useQuery(
{
organizationId: organizationId ?? "",
},
{
enabled: !!organizationId,
},
);
const { mutateAsync, isLoading } = organizationId
? api.organization.update.useMutation()
: api.organization.create.useMutation();
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
useEffect(() => {
if (organization) {
setName(organization.name);
}
}, [organization]);
const handleSubmit = async () => {
await mutateAsync({ name, organizationId: organizationId ?? "" })
.then(() => {
setOpen(false);
toast.success(
`Organization ${organizationId ? "updated" : "created"} successfully`,
);
utils.organization.all.invalidate();
})
.catch((error) => {
console.error(error);
toast.error(
`Failed to ${organizationId ? "update" : "create"} organization`,
);
});
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{organizationId ? (
<DropdownMenuItem
className="group cursor-pointer hover:bg-blue-500/10"
onSelect={(e) => e.preventDefault()}
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</DropdownMenuItem>
) : (
<DropdownMenuItem
className="gap-2 p-2"
onClick={() => {
setOpen(true);
}}
onSelect={(e) => e.preventDefault()}
>
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">
Add organization
</div>
</DropdownMenuItem>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>
{organizationId ? "Update organization" : "Add organization"}
</DialogTitle>
<DialogDescription>
{organizationId
? "Update the organization name"
: "Create a new organization to manage your projects."}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={handleSubmit} isLoading={isLoading}>
{organizationId ? "Update organization" : "Create organization"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -21,6 +21,7 @@ import {
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, SquarePen } from "lucide-react";
@@ -97,6 +98,18 @@ export const HandleProject = ({ projectId }: Props) => {
);
});
};
// useEffect(() => {
// const getUsers = async () => {
// const users = await authClient.admin.listUsers({
// query: {
// limit: 100,
// },
// });
// console.log(users);
// };
// getUsers();
// });
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>

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

@@ -1,5 +1,6 @@
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import {
AlertDialog,
AlertDialogAction,
@@ -50,15 +51,7 @@ import { ProjectEnvironment } from "./project-environment";
export const ShowProjects = () => {
const utils = api.useUtils();
const { data, isLoading } = api.project.all.useQuery();
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
const [searchQuery, setSearchQuery] = useState("");
@@ -90,7 +83,7 @@ export const ShowProjects = () => {
</CardDescription>
</CardHeader>
{(auth?.rol === "admin" || user?.canCreateProjects) && (
{(auth?.role === "owner" || auth?.user?.canCreateProjects) && (
<div className="">
<HandleProject />
</div>
@@ -176,8 +169,11 @@ export const ShowProjects = () => {
<div key={app.applicationId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs">
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{app.name}
<StatusTooltip
status={app.applicationStatus}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{app.domains.map((domain) => (
@@ -209,8 +205,11 @@ export const ShowProjects = () => {
<div key={comp.composeId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs">
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{comp.name}
<StatusTooltip
status={comp.composeStatus}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{comp.domains.map((domain) => (
@@ -286,8 +285,8 @@ export const ShowProjects = () => {
<div
onClick={(e) => e.stopPropagation()}
>
{(auth?.rol === "admin" ||
user?.canDeleteProjects) && (
{(auth?.role === "owner" ||
auth?.user?.canDeleteProjects) && (
<AlertDialog>
<AlertDialogTrigger className="w-full">
<DropdownMenuItem

View File

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

View File

@@ -18,6 +18,7 @@ import {
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { authClient } from "@/lib/auth-client";
import {
type Services,
extractServices,
@@ -35,8 +36,10 @@ export const SearchCommand = () => {
const router = useRouter();
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState("");
const { data } = api.project.all.useQuery();
const { data: session } = authClient.useSession();
const { data } = api.project.all.useQuery(undefined, {
enabled: !!session,
});
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
React.useEffect(() => {

View File

@@ -39,7 +39,7 @@ export const calculatePrice = (count: number, isAnnual = false) => {
};
export const ShowBilling = () => {
const { data: servers } = api.server.all.useQuery(undefined);
const { data: admin } = api.admin.one.useQuery();
const { data: admin } = api.user.get.useQuery();
const { data, isLoading } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation();
@@ -70,7 +70,7 @@ export const ShowBilling = () => {
return isAnnual ? interval === "year" : interval === "month";
});
const maxServers = admin?.serversQuantity ?? 1;
const maxServers = admin?.user.serversQuantity ?? 1;
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
const safePercentage = Math.min(percentage, 100);
@@ -98,17 +98,17 @@ export const ShowBilling = () => {
<TabsTrigger value="annual">Annual</TabsTrigger>
</TabsList>
</Tabs>
{admin?.stripeSubscriptionId && (
{admin?.user.stripeSubscriptionId && (
<div className="space-y-2 flex flex-col">
<h3 className="text-lg font-medium">Servers Plan</h3>
<p className="text-sm text-muted-foreground">
You have {servers?.length} server on your plan of{" "}
{admin?.serversQuantity} servers
{admin?.user.serversQuantity} servers
</p>
<div>
<Progress value={safePercentage} className="max-w-lg" />
</div>
{admin && admin.serversQuantity! <= servers?.length! && (
{admin && admin.user.serversQuantity! <= servers?.length! && (
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">
@@ -279,7 +279,7 @@ export const ShowBilling = () => {
"flex flex-row items-center gap-2 mt-4",
)}
>
{admin?.stripeCustomerId && (
{admin?.user.stripeCustomerId && (
<Button
variant="secondary"
className="w-full"

View File

@@ -10,12 +10,12 @@ import type React from "react";
import { useEffect, useState } from "react";
export const ShowWelcomeDokploy = () => {
const { data } = api.auth.get.useQuery();
const { data } = api.user.get.useQuery();
const [open, setOpen] = useState(false);
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
if (!isCloud || data?.rol !== "admin") {
if (!isCloud || data?.role !== "admin") {
return null;
}
@@ -24,14 +24,14 @@ export const ShowWelcomeDokploy = () => {
!isLoading &&
isCloud &&
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
data?.rol === "admin"
data?.role === "owner"
) {
setOpen(true);
}
}, [isCloud, isLoading]);
const handleClose = (isOpen: boolean) => {
if (data?.rol === "admin") {
if (data?.role === "owner") {
setOpen(isOpen);
if (!isOpen) {
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal

View File

@@ -86,6 +86,7 @@ export const AddCertificate = () => {
privateKey: data.privateKey,
autoRenew: data.autoRenew,
serverId: data.serverId,
organizationId: "",
})
.then(async () => {
toast.success("Certificate Created");

View File

@@ -53,7 +53,7 @@ export const AddBitbucketProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
const { data: auth } = api.auth.get.useQuery();
const { data: auth } = api.user.get.useQuery();
const router = useRouter();
const form = useForm<Schema>({
defaultValues: {

View File

@@ -10,13 +10,15 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { format } from "date-fns";
import { useEffect, useState } from "react";
export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const { data } = api.auth.get.useQuery();
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data } = api.user.get.useQuery();
const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false);
const [organizationName, setOrganization] = useState("");
@@ -25,7 +27,7 @@ export const AddGithubProvider = () => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/providers/github/setup?authId=${data?.id}`,
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin,
hook_attributes: {
@@ -93,8 +95,8 @@ export const AddGithubProvider = () => {
<form
action={
isOrganization
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
: `https://github.com/settings/apps/new?state=gh_init:${data?.id}`
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}`
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}`
}
method="post"
>

View File

@@ -55,7 +55,7 @@ export const AddGitlabProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data: auth } = api.auth.get.useQuery();
const { data: auth } = api.user.get.useQuery();
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
const webhookUrl = `${url}/api/providers/gitlab/callback`;

View File

@@ -49,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", [
@@ -204,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({
@@ -216,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({
@@ -228,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({
@@ -244,6 +248,7 @@ 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({
@@ -280,6 +285,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dokployRestart,
databaseBackup,
dockerCleanup,
serverThreshold,
} = data;
let promise: Promise<unknown> | null = null;
if (data.type === "slack") {
@@ -294,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({
@@ -307,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({
@@ -320,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({
@@ -337,6 +346,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
emailId: notification?.emailId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "gotify") {
promise = gotifyMutation.mutateAsync({
@@ -955,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>

View File

@@ -1,52 +1,134 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const PasswordSchema = z.object({
password: z.string().min(8, {
message: "Password is required",
}),
});
type PasswordForm = z.infer<typeof PasswordSchema>;
export const Disable2FA = () => {
const utils = api.useUtils();
const { mutateAsync, isLoading } = api.auth.disable2FA.useMutation();
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const form = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const handleSubmit = async (formData: PasswordForm) => {
setIsLoading(true);
try {
const result = await authClient.twoFactor.disable({
password: formData.password,
});
if (result.error) {
form.setError("password", {
message: result.error.message,
});
toast.error(result.error.message);
return;
}
toast.success("2FA disabled successfully");
utils.auth.get.invalidate();
setIsOpen(false);
} catch (error) {
form.setError("password", {
message: "Connection error. Please try again.",
});
toast.error("Connection error. Please try again.");
} finally {
setIsLoading(false);
}
};
return (
<AlertDialog>
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Disable 2FA
</Button>
<Button variant="destructive">Disable 2FA</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the 2FA
This action cannot be undone. This will permanently disable
Two-Factor Authentication for your account.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync()
.then(() => {
utils.auth.get.invalidate();
toast.success("2FA Disabled");
})
.catch(() => {
toast.error("Error disabling 2FA");
});
}}
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormDescription>
Enter your password to disable 2FA
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={() => {
form.reset();
setIsOpen(false);
}}
>
Cancel
</Button>
<Button type="submit" variant="destructive" isLoading={isLoading}>
Disable 2FA
</Button>
</div>
</form>
</Form>
</AlertDialogContent>
</AlertDialog>
);

View File

@@ -17,144 +17,315 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Fingerprint } from "lucide-react";
import { useEffect } from "react";
import { Fingerprint, QrCode } from "lucide-react";
import QRCode from "qrcode";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Enable2FASchema = z.object({
const PasswordSchema = z.object({
password: z.string().min(8, {
message: "Password is required",
}),
});
const PinSchema = z.object({
pin: z.string().min(6, {
message: "Pin is required",
}),
});
type Enable2FA = z.infer<typeof Enable2FASchema>;
type PasswordForm = z.infer<typeof PasswordSchema>;
type PinForm = z.infer<typeof PinSchema>;
type TwoFactorEnableResponse = {
totpURI: string;
backupCodes: string[];
};
type TwoFactorSetupData = {
qrCodeUrl: string;
secret: string;
totpURI: string;
};
export const Enable2FA = () => {
const utils = api.useUtils();
const { data: session } = authClient.useSession();
const [data, setData] = useState<TwoFactorSetupData | null>(null);
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [step, setStep] = useState<"password" | "verify">("password");
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const { data } = api.auth.generate2FASecret.useQuery(undefined, {
refetchOnWindowFocus: false,
const handlePasswordSubmit = async (formData: PasswordForm) => {
setIsPasswordLoading(true);
try {
const { data: enableData } = await authClient.twoFactor.enable({
password: formData.password,
});
if (!enableData) {
throw new Error("No data received from server");
}
if (enableData.backupCodes) {
setBackupCodes(enableData.backupCodes);
}
if (enableData.totpURI) {
const qrCodeUrl = await QRCode.toDataURL(enableData.totpURI);
setData({
qrCodeUrl,
secret: enableData.totpURI.split("secret=")[1]?.split("&")[0] || "",
totpURI: enableData.totpURI,
});
setStep("verify");
toast.success("Scan the QR code with your authenticator app");
} else {
throw new Error("No TOTP URI received from server");
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error setting up 2FA",
);
passwordForm.setError("password", {
message: "Error verifying password",
});
} finally {
setIsPasswordLoading(false);
}
};
const handleVerifySubmit = async (formData: PinForm) => {
try {
const result = await authClient.twoFactor.verifyTotp({
code: formData.pin,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
pinForm.setError("pin", {
message: "Invalid code. Please try again.",
});
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.auth.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
pinForm.setError("pin", {
message: errorMessage,
});
toast.error(errorMessage);
} else {
pinForm.setError("pin", {
message: "Error verifying code",
});
toast.error("Error verifying 2FA code");
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const { mutateAsync, isLoading, error, isError } =
api.auth.verify2FASetup.useMutation();
const form = useForm<Enable2FA>({
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
resolver: zodResolver(Enable2FASchema),
});
useEffect(() => {
form.reset({
pin: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
passwordForm.reset();
pinForm.reset();
}
}, [isDialogOpen, passwordForm, pinForm]);
const onSubmit = async (formData: Enable2FA) => {
await mutateAsync({
pin: formData.pin,
secret: data?.secret || "",
})
.then(async () => {
toast.success("2FA Verified");
utils.auth.get.invalidate();
})
.catch(() => {
toast.error("Error verifying the 2FA");
});
};
return (
<Dialog>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<Fingerprint className="size-4 text-muted-foreground" />
Enable 2FA
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen max-sm:overflow-y-auto sm:max-w-xl ">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
<DialogHeader>
<DialogTitle>2FA Setup</DialogTitle>
<DialogDescription>Add a 2FA to your account</DialogDescription>
<DialogDescription>
{step === "password"
? "Enter your password to begin 2FA setup"
: "Scan the QR code and verify with your authenticator app"}
</DialogDescription>
</DialogHeader>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<Form {...form}>
<form
id="hook-form-add-2FA"
onSubmit={form.handleSubmit(onSubmit)}
className="grid sm:grid-cols-2 w-full gap-4"
>
<div className="flex flex-col gap-4 justify-center items-center">
<span className="text-sm text-muted-foreground">
{data?.qrCodeUrl ? "Scan the QR code to add 2FA" : ""}
</span>
<img
src={data?.qrCodeUrl}
alt="qrCode"
className="rounded-lg w-fit"
/>
<div className="flex flex-col gap-2">
<span className="text-sm text-muted-foreground text-center">
{data?.secret ? `Secret: ${data?.secret}` : ""}
</span>
</div>
</div>
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Pin</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription className="max-md:text-center">
Please enter the 6 digits code provided by your
authenticator app.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-add-2FA"
type="submit"
{step === "password" ? (
<Form {...passwordForm}>
<form
id="password-form"
onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)}
className="space-y-4"
>
Submit 2FA
</Button>
</DialogFooter>
</Form>
<FormField
control={passwordForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormDescription>
Enter your password to enable 2FA
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
isLoading={isPasswordLoading}
>
Continue
</Button>
</form>
</Form>
) : (
<Form {...pinForm}>
<form
id="pin-form"
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
className="space-y-6"
>
<div className="flex flex-col gap-6 justify-center items-center">
{data?.qrCodeUrl ? (
<>
<div className="flex flex-col items-center gap-4 p-6 border rounded-lg">
<QrCode className="size-5 text-muted-foreground" />
<span className="text-sm font-medium">
Scan this QR code with your authenticator app
</span>
<img
src={data.qrCodeUrl}
alt="2FA QR Code"
className="rounded-lg w-48 h-48"
/>
<div className="flex flex-col gap-2 text-center">
<span className="text-sm text-muted-foreground">
Can't scan the QR code?
</span>
<span className="text-xs font-mono bg-muted p-2 rounded">
{data.secret}
</span>
</div>
</div>
{backupCodes && backupCodes.length > 0 && (
<div className="w-full space-y-3 border rounded-lg p-4">
<h4 className="font-medium">Backup Codes</h4>
<div className="grid grid-cols-2 gap-2">
{backupCodes.map((code, index) => (
<code
key={index}
className="bg-muted p-2 rounded text-sm font-mono"
>
{code}
</code>
))}
</div>
<p className="text-sm text-muted-foreground">
Save these backup codes in a secure place. You can use
them to access your account if you lose access to your
authenticator device.
</p>
</div>
)}
</>
) : (
<div className="flex items-center justify-center w-full h-48 bg-muted rounded-lg">
<QrCode className="size-8 text-muted-foreground animate-pulse" />
</div>
)}
</div>
<FormField
control={pinForm.control}
name="pin"
render={({ field }) => (
<FormItem className="flex flex-col justify-center items-center">
<FormLabel>Verification Code</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Enter the 6-digit code from your authenticator app
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
isLoading={isPasswordLoading}
>
Enable 2FA
</Button>
</form>
</Form>
)}
</DialogContent>
</Dialog>
);

View File

@@ -14,7 +14,7 @@ import Link from "next/link";
import { toast } from "sonner";
export const GenerateToken = () => {
const { data, refetch } = api.auth.get.useQuery();
const { data, refetch } = api.user.get.useQuery();
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
api.auth.generateToken.useMutation();
@@ -51,7 +51,7 @@ export const GenerateToken = () => {
<Label>Token</Label>
<ToggleVisibilityInput
placeholder="Token"
value={data?.token || ""}
value={data?.user?.token || ""}
disabled
/>
</div>

View File

@@ -1,4 +1,5 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -17,6 +18,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { authClient } from "@/lib/auth-client";
import { generateSHA256Hash } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -54,7 +56,10 @@ const randomImages = [
];
export const ProfileForm = () => {
const { data, refetch, isLoading } = api.auth.get.useQuery();
const utils = api.useUtils();
const { mutateAsync: disable2FA, isLoading: isDisabling } =
api.auth.disable2FA.useMutation();
const { data, refetch, isLoading } = api.user.get.useQuery();
const {
mutateAsync,
isLoading: isUpdating,
@@ -73,9 +78,9 @@ export const ProfileForm = () => {
const form = useForm<Profile>({
defaultValues: {
email: data?.email || "",
email: data?.user?.email || "",
password: "",
image: data?.image || "",
image: data?.user?.image || "",
currentPassword: "",
},
resolver: zodResolver(profileSchema),
@@ -84,14 +89,14 @@ export const ProfileForm = () => {
useEffect(() => {
if (data) {
form.reset({
email: data?.email || "",
email: data?.user?.email || "",
password: "",
image: data?.image || "",
image: data?.user?.image || "",
currentPassword: "",
});
if (data.email) {
generateSHA256Hash(data.email).then((hash) => {
if (data.user.email) {
generateSHA256Hash(data.user.email).then((hash) => {
setGravatarHash(hash);
});
}
@@ -130,7 +135,7 @@ export const ProfileForm = () => {
{t("settings.profile.description")}
</CardDescription>
</div>
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Disable2FA />}
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">

View File

@@ -35,7 +35,7 @@ const profileSchema = z.object({
type Profile = z.infer<typeof profileSchema>;
export const RemoveSelfAccount = () => {
const { data } = api.auth.get.useQuery();
const { data } = api.user.get.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.auth.removeSelfAccount.useMutation();
const { t } = useTranslation("settings");

View File

@@ -7,7 +7,7 @@ interface Props {
serverId?: string;
}
export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.admin.one.useQuery(undefined, {
const { data, refetch } = api.user.get.useQuery(undefined, {
enabled: !serverId,
});
@@ -20,7 +20,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
},
);
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();

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.user.get.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";
@@ -314,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

@@ -35,6 +35,7 @@ export const CreateSSHKey = () => {
description: "Used on Dokploy Cloud",
privateKey: keys.privateKey,
publicKey: keys.publicKey,
organizationId: "",
});
await refetch();
} catch (error) {

View File

@@ -78,6 +78,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
const onSubmit = async (data: SSHKey) => {
await mutateAsync({
...data,
organizationId: "",
sshKeyId: sshKeyId || "",
})
.then(async () => {

View File

@@ -19,6 +19,14 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
@@ -27,62 +35,70 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addUser = z.object({
const addInvitation = z.object({
email: z
.string()
.min(1, "Email is required")
.email({ message: "Invalid email" }),
role: z.enum(["member", "admin"]),
});
type AddUser = z.infer<typeof addUser>;
type AddInvitation = z.infer<typeof addInvitation>;
export const AddUser = () => {
export const AddInvitation = () => {
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { data: activeOrganization } = authClient.useActiveOrganization();
const { mutateAsync, isError, error, isLoading } =
api.admin.createUserInvitation.useMutation();
const form = useForm<AddUser>({
const form = useForm<AddInvitation>({
defaultValues: {
email: "",
role: "member",
},
resolver: zodResolver(addUser),
resolver: zodResolver(addInvitation),
});
useEffect(() => {
form.reset();
}, [form, form.formState.isSubmitSuccessful, form.reset]);
const onSubmit = async (data: AddUser) => {
await mutateAsync({
const onSubmit = async (data: AddInvitation) => {
setIsLoading(true);
const result = await authClient.organization.inviteMember({
email: data.email.toLowerCase(),
})
.then(async () => {
toast.success("Invitation created");
await utils.user.all.invalidate();
setOpen(false);
})
.catch(() => {
toast.error("Error creating the invitation");
});
role: data.role,
organizationId: activeOrganization?.id,
});
if (result.error) {
setError(result.error.message || "");
} else {
toast.success("Invitation created");
setError(null);
setOpen(false);
}
utils.organization.allInvitations.invalidate();
setIsLoading(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="" asChild>
<Button>
<PlusIcon className="h-4 w-4" /> Add User
<PlusIcon className="h-4 w-4" /> Add Invitation
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Add User</DialogTitle>
<DialogTitle>Add Invitation</DialogTitle>
<DialogDescription>Invite a new user</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{error && <AlertBlock type="error">{error}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-user"
id="hook-form-add-invitation"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
>
@@ -104,10 +120,39 @@ export const AddUser = () => {
);
}}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="member">Member</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Select the role for the new user
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<DialogFooter className="flex w-full flex-row">
<Button
isLoading={isLoading}
form="hook-form-add-user"
form="hook-form-add-invitation"
type="submit"
>
Create

View File

@@ -52,7 +52,7 @@ interface Props {
export const AddUserPermissions = ({ userId }: Props) => {
const { data: projects } = api.project.all.useQuery();
const { data, refetch } = api.user.byUserId.useQuery(
const { data, refetch } = api.auth.one.useQuery(
{
userId,
},
@@ -62,7 +62,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
);
const { mutateAsync, isError, error, isLoading } =
api.admin.assignPermissions.useMutation();
api.user.assignPermissions.useMutation();
const form = useForm<AddPermissions>({
defaultValues: {
@@ -92,7 +92,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
const onSubmit = async (data: AddPermissions) => {
await mutateAsync({
userId,
id: userId,
canCreateServices: data.canCreateServices,
canCreateProjects: data.canCreateProjects,
canDeleteServices: data.canDeleteServices,

View File

@@ -0,0 +1,208 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { format, isPast } from "date-fns";
import { Mail, MoreHorizontal, Users } from "lucide-react";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { AddInvitation } from "./add-invitation";
export const ShowInvitations = () => {
const { data, isLoading, refetch } =
api.organization.allInvitations.useQuery();
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<Mail className="size-6 text-muted-foreground self-center" />
Invitations
</CardTitle>
<CardDescription>
Create invitations to your organization.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<>
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<Users className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground">
Invite users to your organization
</span>
<AddInvitation />
</div>
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
<Table>
<TableCaption>See all invitations</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Email</TableHead>
<TableHead className="text-center">Role</TableHead>
<TableHead className="text-center">Status</TableHead>
<TableHead className="text-center">
Expires At
</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((invitation) => {
const isExpired = isPast(
new Date(invitation.expiresAt),
);
return (
<TableRow key={invitation.id}>
<TableCell className="w-[100px]">
{invitation.email}
</TableCell>
<TableCell className="text-center">
<Badge
variant={
invitation.role === "owner"
? "default"
: "secondary"
}
>
{invitation.role}
</Badge>
</TableCell>
<TableCell className="text-center">
<Badge
variant={
invitation.status === "pending"
? "secondary"
: invitation.status === "canceled"
? "destructive"
: "default"
}
>
{invitation.status}
</Badge>
</TableCell>
<TableCell className="text-center">
{format(new Date(invitation.expiresAt), "PPpp")}{" "}
{isExpired ? (
<span className="text-muted-foreground">
(Expired)
</span>
) : null}
</TableCell>
<TableCell className="text-right flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>
Actions
</DropdownMenuLabel>
{!isExpired && (
<>
{invitation.status === "pending" && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => {
copy(
`${origin}/invitation?token=${invitation.id}`,
);
toast.success(
"Invitation Copied to clipboard",
);
}}
>
Copy Invitation
</DropdownMenuItem>
)}
{invitation.status === "pending" && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (e) => {
const result =
await authClient.organization.cancelInvitation(
{
invitationId: invitation.id,
},
);
if (result.error) {
toast.error(
result.error.message,
);
} else {
toast.success(
"Invitation deleted",
);
refetch();
}
}}
>
Cancel Invitation
</DropdownMenuItem>
)}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<AddInvitation />
</div>
</div>
)}
</>
)}
</CardContent>
</div>
</Card>
</div>
);
};

View File

@@ -1,3 +1,4 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -23,22 +24,19 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { format } from "date-fns";
import { MoreHorizontal, Users } from "lucide-react";
import { useEffect, useState } from "react";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { AddUserPermissions } from "./add-permissions";
import { AddUser } from "./add-user";
import { DialogAction } from "@/components/shared/dialog-action";
import { Loader2 } from "lucide-react";
export const ShowUsers = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data, isLoading, refetch } = api.user.all.useQuery();
const { mutateAsync, isLoading: isRemoving } =
api.admin.removeUser.useMutation();
const { mutateAsync, isLoading: isRemoving } = api.user.remove.useMutation();
return (
<div className="w-full">
@@ -67,7 +65,6 @@ export const ShowUsers = () => {
<span className="text-base text-muted-foreground">
Invite users to your Dokploy account
</span>
<AddUser />
</div>
) : (
<div className="flex flex-col gap-4 min-h-[25vh]">
@@ -76,43 +73,41 @@ export const ShowUsers = () => {
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Email</TableHead>
<TableHead className="text-center">Status</TableHead>
<TableHead className="text-center">Role</TableHead>
<TableHead className="text-center">2FA</TableHead>
<TableHead className="text-center">
Expiration
Created At
</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((user) => {
{data?.map((member) => {
return (
<TableRow key={user.userId}>
<TableRow key={member.id}>
<TableCell className="w-[100px]">
{user.auth.email}
{member.user.email}
</TableCell>
<TableCell className="text-center">
<Badge
variant={
user.isRegistered ? "default" : "secondary"
member.role === "owner"
? "default"
: "secondary"
}
>
{user.isRegistered
? "Registered"
: "Not Registered"}
{member.role}
</Badge>
</TableCell>
<TableCell className="text-center">
{user.auth.is2FAEnabled
? "2FA Enabled"
: "2FA Not Enabled"}
{member.user.twoFactorEnabled
? "Enabled"
: "Disabled"}
</TableCell>
<TableCell className="text-right">
<TableCell className="text-center">
<span className="text-sm text-muted-foreground">
{format(
new Date(user.expirationDate),
"PPpp",
)}
{format(new Date(member.createdAt), "PPpp")}
</span>
</TableCell>
@@ -131,56 +126,63 @@ export const ShowUsers = () => {
<DropdownMenuLabel>
Actions
</DropdownMenuLabel>
{!user.isRegistered && (
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => {
copy(
`${origin}/invitation?token=${user.token}`,
);
toast.success(
"Invitation Copied to clipboard",
);
}}
>
Copy Invitation
</DropdownMenuItem>
)}
{user.isRegistered && (
{member.role !== "owner" && (
<AddUserPermissions
userId={user.userId}
userId={member.user.id}
/>
)}
<DialogAction
title="Delete User"
description="Are you sure you want to delete this user?"
type="destructive"
onClick={async () => {
await mutateAsync({
authId: user.authId,
})
.then(() => {
toast.success(
"User deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting destination",
);
});
}}
>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
{member.role !== "owner" && (
<DialogAction
title="Delete User"
description="Are you sure you want to delete this user?"
type="destructive"
onClick={async () => {
if (isCloud) {
const { error } =
await authClient.organization.removeMember(
{
memberIdOrEmail: member.id,
},
);
if (!error) {
toast.success(
"User deleted successfully",
);
refetch();
} else {
toast.error(
"Error deleting user",
);
}
} else {
await mutateAsync({
userId: member.user.id,
})
.then(() => {
toast.success(
"User deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting destination",
);
});
}
}}
>
Delete User
</DropdownMenuItem>
</DialogAction>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
Delete User
</DropdownMenuItem>
</DialogAction>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
@@ -189,10 +191,6 @@ export const ShowUsers = () => {
})}
</TableBody>
</Table>
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
<AddUser />
</div>
</div>
)}
</>

View File

@@ -52,7 +52,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => {
const { t } = useTranslation("settings");
const { data: user, refetch } = api.admin.one.useQuery();
const { data, refetch } = api.user.get.useQuery();
const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation();
@@ -65,14 +65,14 @@ export const WebDomain = () => {
resolver: zodResolver(addServerDomain),
});
useEffect(() => {
if (user) {
if (data) {
form.reset({
domain: user?.host || "",
certificateType: user?.certificateType,
letsEncryptEmail: user?.letsEncryptEmail || "",
domain: data?.user?.host || "",
certificateType: data?.user?.certificateType,
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
});
}
}, [form, form.reset, user]);
}, [form, form.reset, data]);
const onSubmit = async (data: AddServerDomain) => {
await mutateAsync({

View File

@@ -21,7 +21,7 @@ interface Props {
}
export const WebServer = ({ className }: Props) => {
const { t } = useTranslation("settings");
const { data } = api.admin.one.useQuery();
const { data } = api.user.get.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
@@ -58,7 +58,7 @@ export const WebServer = ({ className }: Props) => {
<div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground">
Server IP: {data?.serverIp}
Server IP: {data?.user.serverIp}
</span>
<span className="text-sm text-muted-foreground">
Version: {dokployVersion}

View File

@@ -1,5 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -47,15 +46,15 @@ interface Props {
export const UpdateServerIp = ({ children, serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data } = api.admin.one.useQuery();
const { data } = api.user.get.useQuery();
const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.admin.update.useMutation();
api.user.update.useMutation();
const form = useForm<Schema>({
defaultValues: {
serverIp: data?.serverIp || "",
serverIp: data?.user.serverIp || "",
},
resolver: zodResolver(schema),
});
@@ -63,7 +62,7 @@ export const UpdateServerIp = ({ children, serverId }: Props) => {
useEffect(() => {
if (data) {
form.reset({
serverIp: data.serverIp || "",
serverIp: data.user.serverIp || "",
});
}
}, [form, form.reset, data]);

View File

@@ -15,7 +15,7 @@ export const OnboardingLayout = ({ children }: Props) => {
<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

View File

@@ -1,6 +1,7 @@
"use client";
import {
Activity,
AudioWaveform,
BarChartHorizontalBigIcon,
Bell,
BlocksIcon,
@@ -8,6 +9,7 @@ import {
Boxes,
ChevronRight,
CircleHelp,
Command,
CreditCard,
Database,
Folder,
@@ -16,11 +18,13 @@ import {
GitBranch,
HeartIcon,
KeyRound,
Loader2,
type LucideIcon,
Package,
PieChart,
Server,
ShieldCheck,
Trash2,
User,
Users,
} from "lucide-react";
@@ -74,7 +78,6 @@ import { UserNav } from "./user-nav";
// The types of the queries we are going to use
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
type UserQueryOutput = inferRouterOutputs<AppRouter>["user"]["byAuthId"];
type SingleNavItem = {
isSingle?: true;
@@ -83,7 +86,6 @@ type SingleNavItem = {
icon?: LucideIcon;
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
@@ -101,7 +103,6 @@ type NavItem =
items: SingleNavItem[];
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
@@ -114,7 +115,6 @@ type ExternalLink = {
icon: React.ComponentType<{ className?: string }>;
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
@@ -145,7 +145,7 @@ const MENU: Menu = {
url: "/dashboard/monitoring",
icon: BarChartHorizontalBigIcon,
// Only enabled in non-cloud environments
isEnabled: ({ auth, user, isCloud }) => !isCloud,
isEnabled: ({ auth, isCloud }) => !isCloud,
},
{
isSingle: true,
@@ -153,9 +153,9 @@ const MENU: Menu = {
url: "/dashboard/traefik",
icon: GalleryVerticalEnd,
// Only enabled for admins and users with access to Traefik files in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
isEnabled: ({ auth, isCloud }) =>
!!(
(auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
(auth?.role === "owner" || auth?.user?.canAccessToTraefikFiles) &&
!isCloud
),
},
@@ -165,8 +165,11 @@ const MENU: Menu = {
url: "/dashboard/docker",
icon: BlocksIcon,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
isEnabled: ({ auth, isCloud }) =>
!!(
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
!isCloud
),
},
{
isSingle: true,
@@ -174,8 +177,11 @@ const MENU: Menu = {
url: "/dashboard/swarm",
icon: PieChart,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
isEnabled: ({ auth, isCloud }) =>
!!(
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
!isCloud
),
},
{
isSingle: true,
@@ -183,8 +189,11 @@ const MENU: Menu = {
url: "/dashboard/requests",
icon: Forward,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
isEnabled: ({ auth, isCloud }) =>
!!(
(auth?.role === "owner" || auth?.user?.canAccessToDocker) &&
!isCloud
),
},
// Legacy unused menu, adjusted to the new structure
@@ -251,8 +260,7 @@ const MENU: Menu = {
url: "/dashboard/settings/server",
icon: Activity,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && !isCloud),
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
},
{
isSingle: true,
@@ -266,7 +274,7 @@ const MENU: Menu = {
url: "/dashboard/settings/servers",
icon: Server,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -274,7 +282,7 @@ const MENU: Menu = {
icon: Users,
url: "/dashboard/settings/users",
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -282,8 +290,8 @@ const MENU: Menu = {
icon: KeyRound,
url: "/dashboard/settings/ssh-keys",
// Only enabled for admins and users with access to SSH keys
isEnabled: ({ auth, user }) =>
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
isEnabled: ({ auth }) =>
!!(auth?.role === "owner" || auth?.user?.canAccessToSSHKeys),
},
{
isSingle: true,
@@ -291,8 +299,8 @@ const MENU: Menu = {
url: "/dashboard/settings/git-providers",
icon: GitBranch,
// Only enabled for admins and users with access to Git providers
isEnabled: ({ auth, user }) =>
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
isEnabled: ({ auth }) =>
!!(auth?.role === "owner" || auth?.user?.canAccessToGitProviders),
},
{
isSingle: true,
@@ -300,7 +308,7 @@ const MENU: Menu = {
url: "/dashboard/settings/registry",
icon: Package,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -308,7 +316,7 @@ const MENU: Menu = {
url: "/dashboard/settings/destinations",
icon: Database,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
@@ -317,7 +325,7 @@ const MENU: Menu = {
url: "/dashboard/settings/certificates",
icon: ShieldCheck,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -325,8 +333,7 @@ const MENU: Menu = {
url: "/dashboard/settings/cluster",
icon: Boxes,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && !isCloud),
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
},
{
isSingle: true,
@@ -334,7 +341,7 @@ const MENU: Menu = {
url: "/dashboard/settings/notifications",
icon: Bell,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
},
{
isSingle: true,
@@ -342,8 +349,7 @@ const MENU: Menu = {
url: "/dashboard/settings/billing",
icon: CreditCard,
// Only enabled for admins in cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && isCloud),
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
},
],
@@ -379,7 +385,6 @@ const MENU: Menu = {
*/
function createMenuForAuthUser(opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}): Menu {
return {
@@ -390,7 +395,6 @@ function createMenuForAuthUser(opts: {
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
@@ -401,7 +405,6 @@ function createMenuForAuthUser(opts: {
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
@@ -412,7 +415,6 @@ function createMenuForAuthUser(opts: {
? true
: item.isEnabled({
auth: opts.auth,
user: opts.user,
isCloud: opts.isCloud,
}),
),
@@ -480,37 +482,218 @@ interface Props {
function LogoWrapper() {
return <SidebarLogo />;
}
import { ChevronsUpDown, Plus } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client";
import { toast } from "sonner";
import { AddOrganization } from "../dashboard/organization/handle-organization";
import { DialogAction } from "../shared/dialog-action";
import { Button } from "../ui/button";
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
teams: [
{
name: "Acme Inc",
logo: GalleryVerticalEnd,
plan: "Enterprise",
},
{
name: "Acme Corp.",
logo: AudioWaveform,
plan: "Startup",
},
{
name: "Evil Corp.",
logo: Command,
plan: "Free",
},
],
};
function SidebarLogo() {
const { state } = useSidebar();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.get.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { data: session } = authClient.useSession();
const {
data: organizations,
refetch,
isLoading,
} = api.organization.all.useQuery();
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
api.organization.delete.useMutation();
const { isMobile } = useSidebar();
const { data: activeOrganization } = authClient.useActiveOrganization();
const [activeTeam, setActiveTeam] = useState<
typeof activeOrganization | null
>(null);
useEffect(() => {
if (activeOrganization) {
setActiveTeam(activeOrganization);
}
}, [activeOrganization]);
return (
<Link
href="/dashboard/projects"
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
>
<div
className={cn(
"flex aspect-square items-center justify-center rounded-lg transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
<>
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[5vh] pt-4">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
{/* <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> */}
<div
className={cn(
"flex aspect-square items-center justify-center rounded-lg transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
>
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{activeTeam?.name}
</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Organizations
</DropdownMenuLabel>
{organizations?.map((org, index) => (
<div className="flex flex-row justify-between" key={org.name}>
<DropdownMenuItem
onClick={async () => {
await authClient.organization.setActive({
organizationId: org.id,
});
window.location.reload();
}}
className="w-full gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
{org.name}
</DropdownMenuItem>
{(org.ownerId === session?.user?.id || isCloud) && (
<div className="flex items-center gap-2">
<AddOrganization organizationId={org.id} />
<DialogAction
title="Delete Organization"
description="Are you sure you want to delete this organization?"
type="destructive"
onClick={async () => {
await deleteOrganization({
organizationId: org.id,
})
.then(() => {
refetch();
toast.success(
"Organization deleted successfully",
);
})
.catch((error) => {
toast.error(
error?.message ||
"Error deleting organization",
);
});
}}
>
<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>
))}
{!isCloud && user?.role === "owner" && (
<>
<DropdownMenuSeparator />
<AddOrganization />
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)}
{/* <Link
href="/dashboard/projects"
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
>
<Logo
<div
className={cn(
"transition-all",
"flex aspect-square items-center justify-center rounded-lg transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
>
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
<p className="truncate font-semibold">Dokploy</p>
<p className="truncate text-xs text-muted-foreground">
{dokployVersion}
</p>
</div>
</Link>
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
<p className="truncate font-semibold">Dokploy</p>
<p className="truncate text-xs text-muted-foreground">
{dokployVersion}
</p>
</div>
</Link> */}
</>
);
}
@@ -531,15 +714,7 @@ export default function Page({ children }: Props) {
const router = useRouter();
const pathname = usePathname();
const currentPath = router.pathname;
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { data: auth } = api.user.get.useQuery();
const includesProjects = pathname?.includes("/dashboard/project");
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
@@ -548,7 +723,7 @@ export default function Page({ children }: Props) {
home: filteredHome,
settings: filteredSettings,
help,
} = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
const activeItem = findActiveNavItem(
[...filteredHome, ...filteredSettings],
@@ -557,7 +732,7 @@ export default function Page({ children }: Props) {
// const showProjectsButton =
// currentPath === "/dashboard/projects" &&
// (auth?.rol === "admin" || user?.canCreateProjects);
// (auth?.rol === "owner" || user?.canCreateProjects);
return (
<SidebarProvider
@@ -577,12 +752,12 @@ export default function Page({ children }: Props) {
>
<Sidebar collapsible="icon" variant="floating">
<SidebarHeader>
<SidebarMenuButton
{/* <SidebarMenuButton
className="group-data-[collapsible=icon]:!p-0"
size="lg"
>
<LogoWrapper />
</SidebarMenuButton>
> */}
<LogoWrapper />
{/* </SidebarMenuButton> */}
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
@@ -783,7 +958,7 @@ export default function Page({ children }: Props) {
</SidebarMenuButton>
</SidebarMenuItem>
))}
{!isCloud && auth?.rol === "admin" && (
{!isCloud && auth?.role === "owner" && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<UpdateServerButton />

View File

@@ -15,6 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { authClient } from "@/lib/auth-client";
import { Languages } from "@/lib/languages";
import { api } from "@/utils/api";
import useLocale from "@/utils/hooks/use-locale";
@@ -29,18 +30,11 @@ const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const UserNav = () => {
const router = useRouter();
const { data } = api.auth.get.useQuery();
const { data } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: data?.id || "",
},
{
enabled: !!data?.id && data?.rol === "user",
},
);
const { locale, setLocale } = useLocale();
const { mutateAsync } = api.auth.logout.useMutation();
// const { mutateAsync } = api.auth.logout.useMutation();
return (
<DropdownMenu>
@@ -50,12 +44,15 @@ export const UserNav = () => {
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={data?.image || ""} alt={data?.image || ""} />
<AvatarImage
src={data?.user?.image || ""}
alt={data?.user?.image || ""}
/>
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Account</span>
<span className="truncate text-xs">{data?.email}</span>
<span className="truncate text-xs">{data?.user?.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
@@ -70,7 +67,7 @@ export const UserNav = () => {
<DropdownMenuLabel className="flex flex-col">
My Account
<span className="text-xs font-normal text-muted-foreground">
{data?.email}
{data?.user?.email}
</span>
</DropdownMenuLabel>
<ModeToggle />
@@ -95,7 +92,8 @@ export const UserNav = () => {
>
Monitoring
</DropdownMenuItem>
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
{(data?.role === "owner" ||
data?.user?.canAccessToTraefikFiles) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -105,7 +103,7 @@ export const UserNav = () => {
Traefik
</DropdownMenuItem>
)}
{(data?.rol === "admin" || user?.canAccessToDocker) && (
{(data?.role === "owner" || data?.user?.canAccessToDocker) && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -118,7 +116,7 @@ export const UserNav = () => {
</DropdownMenuItem>
)}
{data?.rol === "admin" && (
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -139,7 +137,7 @@ export const UserNav = () => {
>
Profile
</DropdownMenuItem>
{data?.rol === "admin" && (
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -150,7 +148,7 @@ export const UserNav = () => {
</DropdownMenuItem>
)}
{data?.rol === "admin" && (
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -163,7 +161,7 @@ export const UserNav = () => {
</>
)}
</DropdownMenuGroup>
{isCloud && data?.rol === "admin" && (
{isCloud && data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
@@ -178,9 +176,12 @@ export const UserNav = () => {
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await mutateAsync().then(() => {
await authClient.signOut().then(() => {
router.push("/");
});
// await mutateAsync().then(() => {
// router.push("/");
// });
}}
>
Log out

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

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

@@ -0,0 +1 @@
ALTER TABLE "compose" ADD COLUMN "deployable" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "compose" RENAME COLUMN "deployable" TO "isolatedDeployment";

View File

@@ -0,0 +1,136 @@
CREATE TABLE "user_temp" (
"id" text PRIMARY KEY NOT NULL,
"name" text DEFAULT '' NOT NULL,
"token" text NOT NULL,
"isRegistered" boolean DEFAULT false NOT NULL,
"expirationDate" text NOT NULL,
"createdAt" text NOT NULL,
"canCreateProjects" boolean DEFAULT false NOT NULL,
"canAccessToSSHKeys" boolean DEFAULT false NOT NULL,
"canCreateServices" boolean DEFAULT false NOT NULL,
"canDeleteProjects" boolean DEFAULT false NOT NULL,
"canDeleteServices" boolean DEFAULT false NOT NULL,
"canAccessToDocker" boolean DEFAULT false NOT NULL,
"canAccessToAPI" boolean DEFAULT false NOT NULL,
"canAccessToGitProviders" boolean DEFAULT false NOT NULL,
"canAccessToTraefikFiles" boolean DEFAULT false NOT NULL,
"accesedProjects" text[] DEFAULT ARRAY[]::text[] NOT NULL,
"accesedServices" text[] DEFAULT ARRAY[]::text[] NOT NULL,
"two_factor_enabled" boolean DEFAULT false NOT NULL,
"email" text NOT NULL,
"email_verified" boolean NOT NULL,
"image" text,
"banned" boolean,
"ban_reason" text,
"ban_expires" timestamp,
"updated_at" timestamp NOT NULL,
"serverIp" text,
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
"host" text,
"letsEncryptEmail" text,
"sshPrivateKey" text,
"enableDockerCleanup" boolean DEFAULT false NOT NULL,
"enableLogRotation" boolean DEFAULT false NOT NULL,
"enablePaidFeatures" boolean DEFAULT false NOT NULL,
"metricsConfig" jsonb 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 NOT NULL,
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
"stripeCustomerId" text,
"stripeSubscriptionId" text,
"serversQuantity" integer DEFAULT 0 NOT NULL,
CONSTRAINT "user_temp_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "session_temp" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
"impersonated_by" text,
"active_organization_id" text,
CONSTRAINT "session_temp_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp,
"refresh_token_expires_at" timestamp,
"scope" text,
"password" text,
"is2FAEnabled" boolean DEFAULT false NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"resetPasswordToken" text,
"resetPasswordExpiresAt" text,
"confirmationToken" text,
"confirmationExpiresAt" text
);
--> statement-breakpoint
CREATE TABLE "invitation" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"email" text NOT NULL,
"role" text,
"status" text NOT NULL,
"expires_at" timestamp NOT NULL,
"inviter_id" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "member" (
"id" text PRIMARY KEY NOT NULL,
"organization_id" text NOT NULL,
"user_id" text NOT NULL,
"role" text NOT NULL,
"created_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "organization" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"slug" text,
"logo" text,
"created_at" timestamp NOT NULL,
"metadata" text,
"owner_id" text NOT NULL,
CONSTRAINT "organization_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp,
"updated_at" timestamp
);
CREATE TABLE "two_factor" (
"id" text PRIMARY KEY NOT NULL,
"secret" text NOT NULL,
"backup_codes" text NOT NULL,
"user_id" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "certificate" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "notification" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "ssh-key" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "git_provider" ALTER COLUMN "adminId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_temp_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_temp_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint

View File

@@ -0,0 +1,211 @@
-- Custom SQL migration file, put your code below! --
WITH inserted_users AS (
-- Insertar usuarios desde admins
INSERT INTO user_temp (
id,
email,
token,
"email_verified",
"updated_at",
"serverIp",
image,
"certificateType",
host,
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"enableLogRotation",
"enablePaidFeatures",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"stripeCustomerId",
"stripeSubscriptionId",
"serversQuantity",
"expirationDate",
"createdAt",
"isRegistered"
)
SELECT
a."adminId",
auth.email,
COALESCE(auth.token, ''),
true,
CURRENT_TIMESTAMP,
a."serverIp",
auth.image,
a."certificateType",
a.host,
a."letsEncryptEmail",
a."sshPrivateKey",
a."enableDockerCleanup",
a."enableLogRotation",
a."enablePaidFeatures",
a."metricsConfig",
a."cleanupCacheApplications",
a."cleanupCacheOnPreviews",
a."cleanupCacheOnCompose",
a."stripeCustomerId",
a."stripeSubscriptionId",
a."serversQuantity",
NOW() + INTERVAL '1 year',
NOW(),
true
FROM admin a
JOIN auth ON auth.id = a."authId"
RETURNING *
),
inserted_accounts AS (
-- Insertar cuentas para los admins
INSERT INTO account (
id,
"account_id",
"provider_id",
"user_id",
password,
"created_at",
"updated_at"
)
SELECT
gen_random_uuid(),
gen_random_uuid(),
'credential',
a."adminId",
auth.password,
NOW(),
NOW()
FROM admin a
JOIN auth ON auth.id = a."authId"
RETURNING *
),
inserted_orgs AS (
-- Crear organizaciones para cada admin
INSERT INTO organization (
id,
name,
slug,
"owner_id",
"created_at"
)
SELECT
gen_random_uuid(),
'My Organization',
-- Generamos un slug único usando una función de hash
encode(sha256((a."adminId" || CURRENT_TIMESTAMP)::bytea), 'hex'),
a."adminId",
NOW()
FROM admin a
RETURNING *
),
inserted_members AS (
-- Insertar usuarios miembros
INSERT INTO user_temp (
id,
email,
token,
"email_verified",
"updated_at",
image,
"createdAt",
"canAccessToAPI",
"canAccessToDocker",
"canAccessToGitProviders",
"canAccessToSSHKeys",
"canAccessToTraefikFiles",
"canCreateProjects",
"canCreateServices",
"canDeleteProjects",
"canDeleteServices",
"accesedProjects",
"accesedServices",
"expirationDate",
"isRegistered"
)
SELECT
u."userId",
auth.email,
COALESCE(u.token, ''),
true,
CURRENT_TIMESTAMP,
auth.image,
NOW(),
COALESCE(u."canAccessToAPI", false),
COALESCE(u."canAccessToDocker", false),
COALESCE(u."canAccessToGitProviders", false),
COALESCE(u."canAccessToSSHKeys", false),
COALESCE(u."canAccessToTraefikFiles", false),
COALESCE(u."canCreateProjects", false),
COALESCE(u."canCreateServices", false),
COALESCE(u."canDeleteProjects", false),
COALESCE(u."canDeleteServices", false),
COALESCE(u."accesedProjects", '{}'),
COALESCE(u."accesedServices", '{}'),
NOW() + INTERVAL '1 year',
COALESCE(u."isRegistered", false)
FROM "user" u
JOIN admin a ON u."adminId" = a."adminId"
JOIN auth ON auth.id = u."authId"
RETURNING *
),
inserted_member_accounts AS (
-- Insertar cuentas para los usuarios miembros
INSERT INTO account (
id,
"account_id",
"provider_id",
"user_id",
password,
"created_at",
"updated_at"
)
SELECT
gen_random_uuid(),
gen_random_uuid(),
'credential',
u."userId",
auth.password,
NOW(),
NOW()
FROM "user" u
JOIN admin a ON u."adminId" = a."adminId"
JOIN auth ON auth.id = u."authId"
RETURNING *
),
inserted_admin_members AS (
-- Insertar miembros en las organizaciones (admins como owners)
INSERT INTO member (
id,
"organization_id",
"user_id",
role,
"created_at"
)
SELECT
gen_random_uuid(),
o.id,
a."adminId",
'owner',
NOW()
FROM admin a
JOIN inserted_orgs o ON o."owner_id" = a."adminId"
RETURNING *
)
-- Insertar miembros regulares en las organizaciones
INSERT INTO member (
id,
"organization_id",
"user_id",
role,
"created_at"
)
SELECT
gen_random_uuid(),
o.id,
u."userId",
'member',
NOW()
FROM "user" u
JOIN admin a ON u."adminId" = a."adminId"
JOIN inserted_orgs o ON o."owner_id" = a."adminId";

View File

@@ -0,0 +1,32 @@
ALTER TABLE "project" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "destination" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "certificate" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "registry" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "notification" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "ssh-key" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "git_provider" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "server" RENAME COLUMN "adminId" TO "userId";--> statement-breakpoint
ALTER TABLE "project" DROP CONSTRAINT "project_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "destination" DROP CONSTRAINT "destination_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "certificate" DROP CONSTRAINT "certificate_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "registry" DROP CONSTRAINT "registry_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "notification" DROP CONSTRAINT "notification_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "ssh-key" DROP CONSTRAINT "ssh-key_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "server" DROP CONSTRAINT "server_adminId_admin_adminId_fk";
--> statement-breakpoint
ALTER TABLE "project" ADD CONSTRAINT "project_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "destination" ADD CONSTRAINT "destination_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "registry" ADD CONSTRAINT "registry_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "server" ADD CONSTRAINT "server_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "user_temp" ALTER COLUMN "token" SET DEFAULT '';--> statement-breakpoint
ALTER TABLE "user_temp" ADD COLUMN "created_at" timestamp DEFAULT now();

View File

@@ -0,0 +1,16 @@
ALTER TABLE "project" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "destination" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "certificate" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "registry" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "git_provider" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "server" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "project" ADD CONSTRAINT "project_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "destination" ADD CONSTRAINT "destination_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "certificate" ADD CONSTRAINT "certificate_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "registry" ADD CONSTRAINT "registry_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification" ADD CONSTRAINT "notification_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ssh-key" ADD CONSTRAINT "ssh-key_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "server" ADD CONSTRAINT "server_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,142 @@
-- Custom SQL migration file
-- Actualizar projects
UPDATE "project" p
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = p."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE p."organizationId" IS NULL;
-- Actualizar servers
UPDATE "server" s
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = s."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE s."organizationId" IS NULL;
-- Actualizar ssh-keys
UPDATE "ssh-key" k
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = k."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE k."organizationId" IS NULL;
-- Actualizar destinations
UPDATE "destination" d
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = d."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE d."organizationId" IS NULL;
-- Actualizar registry
UPDATE "registry" r
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = r."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE r."organizationId" IS NULL;
-- Actualizar notifications
UPDATE "notification" n
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = n."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE n."organizationId" IS NULL;
-- Actualizar certificates
UPDATE "certificate" c
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = c."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE c."organizationId" IS NULL;
-- Actualizar git_provider
UPDATE "git_provider" g
SET "organizationId" = (
SELECT m."organization_id"
FROM "member" m
WHERE m."user_id" = g."userId"
AND m."role" = 'owner'
LIMIT 1
)
WHERE g."organizationId" IS NULL;
-- Verificar que todos los recursos tengan una organización
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM "project" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "server" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "ssh-key" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "destination" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "registry" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "notification" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "certificate" WHERE "organizationId" IS NULL
UNION ALL
SELECT 1 FROM "git_provider" WHERE "organizationId" IS NULL
) THEN
RAISE EXCEPTION 'Hay recursos sin organización asignada';
END IF;
END $$;
-- Hacer organization_id NOT NULL en todas las tablas
ALTER TABLE "project" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "server" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "ssh-key" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "destination" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "registry" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "notification" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "certificate" ALTER COLUMN "organizationId" SET NOT NULL;
ALTER TABLE "git_provider" ALTER COLUMN "organizationId" SET NOT NULL;
-- Crear índices para mejorar el rendimiento de búsquedas por organización
CREATE INDEX IF NOT EXISTS "idx_project_organization" ON "project" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_server_organization" ON "server" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_sshkey_organization" ON "ssh-key" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_destination_organization" ON "destination" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_registry_organization" ON "registry" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_notification_organization" ON "notification" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_certificate_organization" ON "certificate" ("organizationId");
CREATE INDEX IF NOT EXISTS "idx_git_provider_organization" ON "git_provider" ("organizationId");

View File

@@ -0,0 +1,32 @@
ALTER TABLE "project" DROP CONSTRAINT "project_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "destination" DROP CONSTRAINT "destination_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "certificate" DROP CONSTRAINT "certificate_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "registry" DROP CONSTRAINT "registry_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "notification" DROP CONSTRAINT "notification_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "ssh-key" DROP CONSTRAINT "ssh-key_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "server" DROP CONSTRAINT "server_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "project" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "destination" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "certificate" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "registry" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "notification" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "ssh-key" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "git_provider" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "server" ALTER COLUMN "organizationId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "project" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "destination" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "certificate" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "registry" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "notification" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "ssh-key" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "git_provider" DROP COLUMN "userId";--> statement-breakpoint
ALTER TABLE "server" DROP COLUMN "userId";

View File

@@ -0,0 +1,6 @@
--> statement-breakpoint
DROP TABLE "user" CASCADE;--> statement-breakpoint
DROP TABLE "admin" CASCADE;--> statement-breakpoint
DROP TABLE "auth" CASCADE;--> statement-breakpoint
DROP TABLE "session" CASCADE;--> statement-breakpoint
DROP TYPE "public"."Roles";

View File

@@ -0,0 +1,18 @@
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_organization_id_organization_id_fk";
--> statement-breakpoint
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_inviter_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "member" DROP CONSTRAINT "member_organization_id_organization_id_fk";
--> statement-breakpoint
ALTER TABLE "member" DROP CONSTRAINT "member_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "organization" DROP CONSTRAINT "organization_owner_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_temp_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_temp_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "session_temp" DROP CONSTRAINT "session_temp_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_user_temp_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;

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