Compare commits

..

253 Commits

Author SHA1 Message Date
Mauricio Siu
973b54408e Merge pull request #1027 from Dokploy/canary
v0.16.1
2024-12-29 18:39:17 -06:00
Mauricio Siu
638fbe17a6 refactor: update paths 2024-12-29 18:23:07 -06:00
Mauricio Siu
75e0d0ba78 refactor: add is cloud validation 2024-12-29 18:14:32 -06:00
Mauricio Siu
0f1b911236 chore: bump version 2024-12-29 18:08:20 -06:00
Mauricio Siu
da148a6c50 Merge pull request #959 from szwabodev/feat/localServerTerminal
feat: local server terminal access
2024-12-29 18:06:32 -06:00
Mauricio Siu
c168648cce Merge pull request #1026 from 190km/fix/missing-norwegian-select
fix: missing norwegian lang in select
2024-12-29 17:40:16 -06:00
190km
efd3ad4102 fix: missing norwegian lang in select 2024-12-30 00:30:57 +01:00
Mauricio Siu
a9577395eb Merge pull request #1025 from Dokploy/998-add-unifi-applicaton
feat: add unifi template
2024-12-29 17:19:33 -06:00
Mauricio Siu
1d02d4308f feat: add unifi template 2024-12-29 17:19:17 -06:00
Mauricio Siu
e93e15a9c8 Merge pull request #995 from 190km/refactor-i18n
feat: i18n displays real lang name
2024-12-29 16:14:03 -06:00
Mauricio Siu
511a9beaa5 Merge pull request #1024 from Dokploy/fix/add-fallback-getIp
refactor: add fallback ipv4
2024-12-29 16:13:48 -06:00
Mauricio Siu
ac8c180ba6 refactor: add fallback ipv4 2024-12-29 16:13:06 -06:00
Mauricio Siu
9a2898ba4f Merge pull request #1023 from Dokploy/1008-s3-provider-dropdown-default-value-reset
fix: add provider value in edit destination
2024-12-29 15:56:50 -06:00
Mauricio Siu
2c258c84eb fix: add provider value in edit destination 2024-12-29 15:56:30 -06:00
Mauricio Siu
36511f34f8 Merge branch 'canary' into refactor-i18n 2024-12-29 15:51:17 -06:00
Mauricio Siu
fee5bbc535 Merge pull request #1022 from Dokploy/fix/cron-jobs
fix: add try catch inside of cron jobs
2024-12-29 15:49:28 -06:00
Mauricio Siu
84ad9a5bb3 refactor: revert notifications 2024-12-29 15:45:54 -06:00
Mauricio Siu
69576988eb Merge pull request #1021 from Dokploy/fix/memory-reservation
Fix/memory reservation
2024-12-29 15:36:14 -06:00
Mauricio Siu
d65d050494 fix: add try catch inside of cron jobs 2024-12-29 15:35:15 -06:00
Mauricio Siu
46a5adf793 refactor: remove muted text 2024-12-29 14:17:15 -06:00
Mauricio Siu
5558ee3248 fix: add tooltip and placeholder values 2024-12-29 14:10:41 -06:00
Mauricio Siu
5d8ebd027e fix: parse correctly numbers 2024-12-29 13:53:38 -06:00
Mauricio Siu
9aa49ce9be Merge pull request #1019 from jocubeit/canary
Spelling and grammar corrections
2024-12-29 13:52:31 -06:00
Mauricio Siu
7bcfc17fe9 Merge pull request #1020 from champ3oy/patch-1
convert email to lower case when loging in
2024-12-29 13:34:27 -06:00
cirlorm
5d2d4104f0 convert email to lower case when loging in
I had an issue signing in to my dashboard only to find out it was because the first letter of my email was in caps.
2024-12-29 17:46:44 +00:00
Dom
2af8d6f565 Spelling and grammar corrections 2024-12-29 22:34:57 +10:00
Mauricio Siu
ba45b27608 Merge pull request #1010 from joaotonaco/canary
feat: prevent layout shifting from scrollbar
2024-12-28 19:31:36 -06:00
Mauricio Siu
5aa1c6efb7 Merge pull request #1011 from joaotonaco/fix/inconsistent-service-card-height
fix: inconsistent service card height
2024-12-28 19:30:42 -06:00
Mauricio Siu
e9dad8f9f8 Merge pull request #1016 from 190km/style/swarm
feat: swarm overview style
2024-12-28 19:30:13 -06:00
Mauricio Siu
0c2a382541 Update apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx 2024-12-28 19:29:51 -06:00
190km
81408985d4 feat: swarm overview style 2024-12-29 01:07:47 +01:00
João Gabriel
d45b558251 fix: inconsistent service card height 2024-12-27 11:08:55 -03:00
João Gabriel
a30b1a0cb8 refactor: prevent layout shifting from scrollbar 2024-12-27 10:51:31 -03:00
João Gabriel
c946e3e01b refactor: prevent layout shifting from scrollbar 2024-12-27 10:50:36 -03:00
Mauricio Siu
eb7bae2ef5 Merge pull request #1006 from Dokploy/canary
v0.16.0
2024-12-26 22:52:02 -06:00
Mauricio Siu
4533b193a4 chore: bump version 2024-12-26 22:32:44 -06:00
Mauricio Siu
c22f744e6c Merge pull request #1000 from faytranevozter/feat/simplify-node-logs
Feat: simplify node service log & add badge color on list service/container
2024-12-26 22:23:43 -06:00
Mauricio Siu
d3663eba6b Merge pull request #1005 from Dokploy/fix/swarm-overview
fix: remove unused parsed
2024-12-26 22:18:52 -06:00
Mauricio Siu
f9e4a71144 fix: remove unused parsed 2024-12-26 22:14:18 -06:00
faytranevozter
985b8bc2e0 refactor: extract badgeStateColor function for reuse across log components 2024-12-27 08:48:36 +07:00
Mauricio Siu
87ef889114 Merge pull request #999 from 190km/logs-typo
fix: typo Logssss > Logs
2024-12-26 13:31:57 -06:00
Mauricio Siu
f3494922be Merge pull request #997 from Barsnes/feat/norwegian-lang
feat(i18n): add Norwegian language support
2024-12-26 13:31:21 -06:00
faytranevozter
27252cf58d feat: add badge component to display container state with color coding 2024-12-27 01:44:46 +07:00
faytranevozter
f69fb7684b fix: normalize state string to lowercase and update filter quotes in docker service functions 2024-12-27 01:44:10 +07:00
faytranevozter
20a7995d73 fix: update docker logs command to conditionally include raw flag for swarm 2024-12-27 01:43:52 +07:00
faytranevozter
6df66c3871 fix: correct typo in logs title 2024-12-27 01:34:20 +07:00
usopp
cbfdda1928 fix: typo Logssss > Logs 2024-12-26 17:20:18 +01:00
Tobias Barsnes
766279f265 fix: add norwegian common.json file 2024-12-26 13:30:29 +01:00
Tobias Barsnes
f6e4ae700a feat(i18n): add Norwegian language support 2024-12-26 13:26:16 +01:00
Mauricio Siu
8c8ffe04a7 Merge pull request #926 from thewilloftheshadow/feat/mongo-replica-sets
feat: mongo replica sets
2024-12-25 23:37:29 -06:00
190km
f242f5c65e chore: lint 2024-12-26 01:29:18 +01:00
190km
46348f43f6 chore: lint 2024-12-26 01:26:53 +01:00
190km
c3476a1fdf refactor: refactored i18n to display real lang name 2024-12-26 00:04:50 +01:00
Mauricio Siu
379ea2ac65 Merge pull request #994 from jmsx/canary
feat(i18n): add Spanish language support
2024-12-25 16:34:24 -06:00
Jose Manuel Gonzalez
a172abaee4 feat(i18n): add Spanish language support 2024-12-25 22:44:31 +01:00
Mauricio Siu
a325b293b6 Merge pull request #990 from Dokploy/986-error-to-save-the-deployment
fix: omit macos files and create folders every time
2024-12-25 13:48:48 -06:00
Mauricio Siu
364c2e192e fix: adjust types 2024-12-25 13:45:24 -06:00
Mauricio Siu
d4a3c5cff9 fix: omit macos files and create folders every time 2024-12-25 04:51:49 -06:00
Mauricio Siu
229a9a3a5e Merge pull request #989 from Dokploy/refactor/add-no-trunc
Refactor/add no trunc
2024-12-25 03:54:51 -06:00
Mauricio Siu
5c1993a647 refactor: exclude dokploy services 2024-12-25 03:54:25 -06:00
Mauricio Siu
64a449a09d refactor: add swarm improvements 2024-12-25 03:45:21 -06:00
Mauricio Siu
e65e6d225f refactor: add no trunc 2024-12-25 03:35:21 -06:00
Mauricio Siu
b4fcdc433e feat: add migration 2024-12-25 03:27:46 -06:00
Mauricio Siu
5ce6172187 Merge branch 'canary' into feat/mongo-replica-sets 2024-12-25 03:27:00 -06:00
Mauricio Siu
0affeea5dd remove 2024-12-25 03:26:06 -06:00
Mauricio Siu
8f76d520c8 feat: add replica sets migration 2024-12-25 03:24:18 -06:00
Mauricio Siu
566d9e0bee Merge pull request #931 from faytranevozter/feature/node-cluster-logs
feat: add support for viewing docker logs in swarm mode
2024-12-25 02:52:49 -06:00
Mauricio Siu
da858e215d refactor: add toggle for normal containers & stack 2024-12-25 02:51:44 -06:00
Mauricio Siu
f672c429c4 Merge branch 'canary' into feature/node-cluster-logs 2024-12-25 02:15:14 -06:00
Mauricio Siu
ce34fe3cd8 Merge pull request #985 from Barsnes/chore/autocomplete-off
fix: turn off `autoComplete` for some inputs
2024-12-25 00:48:36 -06:00
Tobias Barsnes
d8dbdb2b9e fix: turn off autoComplete for some inputs 2024-12-25 03:23:02 +01:00
Mauricio Siu
4065ad4428 Update apps/dokploy/components/dashboard/project/add-database.tsx
Co-authored-by: Dominik Koch <dominik@koch-bautechnik.de>
2024-12-24 15:34:09 -06:00
Mauricio Siu
e035062a10 Merge pull request #971 from drudge/cert-enhancements
fix(certificates): ensure certificates are accessible by traefik
2024-12-24 15:33:26 -06:00
Mauricio Siu
36a1daae4b Merge pull request #984 from Dokploy/feat/add-hide-extra-logs
feat: add hide extra logs
2024-12-24 15:13:18 -06:00
Mauricio Siu
830a254837 Merge pull request #979 from drudge/compose-builder-error
fix: show error message when compose builder throws
2024-12-24 15:10:19 -06:00
Mauricio Siu
b8580d69d6 feat: add hide extra logs 2024-12-24 15:09:57 -06:00
Mauricio Siu
e3f1518b0d Merge pull request #977 from drudge/ports-ui
feat(traefik/ports): improved UI
2024-12-24 14:47:58 -06:00
Mauricio Siu
ce19a42aee refactor: use react hook form 2024-12-24 14:47:42 -06:00
Mauricio Siu
28a2ab9aa5 Merge pull request #974 from drudge/preview-deployments
feat(preview-deployments): new UI
2024-12-24 14:31:37 -06:00
Mauricio Siu
0535d780b1 refactor: add missing running state 2024-12-24 14:31:05 -06:00
Mauricio Siu
e1dd666e24 Merge pull request #972 from drudge/traefik-port
fix(setup): use traefik port/version from env
2024-12-24 14:28:10 -06:00
Mauricio Siu
ce71fa4f4d Merge pull request #962 from joaotonaco/feat/discord-emoji-toggle
feat(notifications): discord decoration toggle
2024-12-24 14:13:09 -06:00
Nicholas Penree
364d04f238 fix: show error message when compose builder throws
(cherry picked from commit 3633e3d66ab73a89382d0a2f079af63f57289419)
2024-12-23 22:13:49 -05:00
Nicholas Penree
0db98c0b92 feat(traefik/ports): improved UI 2024-12-23 21:39:16 -05:00
Nicholas Penree
8410d94283 feat(preview-deployments): new UI 2024-12-23 16:55:13 -05:00
Nicholas Penree
56cfd35e7d feat(certificates): clarify you are selecting the cert provider 2024-12-23 12:37:53 -05:00
Nicholas Penree
e7beb5c75b fix(setup): use traefik port/version from env 2024-12-23 10:32:31 -05:00
Nicholas Penree
9c5a61e42f fix(certificates): show proper placeholder for private key format 2024-12-23 09:17:11 -05:00
Nicholas Penree
0ee5a6f13e fix(certificates): ensure certificates are accessible by traefik 2024-12-23 09:16:10 -05:00
João Gabriel
1d35d218ca Merge branch 'canary' into feat/discord-emoji-toggle 2024-12-23 10:08:56 -03:00
Mauricio Siu
96cdffb5b9 Merge pull request #886 from DJKnaeckebrot/feature/delete-docker-volumes
feat: option to delete docker volumes on service deletion
2024-12-23 02:38:28 -06:00
Mauricio Siu
353effd720 refactor: delete log 2024-12-23 02:37:04 -06:00
Mauricio Siu
8bfe1632fa refactor: remove delete volumes 2024-12-23 02:32:43 -06:00
Mauricio Siu
ed543e5397 refactor: lint 2024-12-23 02:28:40 -06:00
djknaeckebrot
c6892ba188 format: fix formatting 2024-12-23 08:39:50 +01:00
djknaeckebrot
fa710d4855 format: fix formatting 2024-12-23 08:39:11 +01:00
djknaeckebrot
375decebb2 chore: remove application delete volume 2024-12-23 08:36:18 +01:00
djknaeckebrot
be2e70a17e chore: only make this apply to compose files 2024-12-23 08:27:45 +01:00
djknaeckebrot
3b4214e040 Merge remote-tracking branch 'origin/feature/delete-docker-volumes' into feature/delete-docker-volumes 2024-12-23 08:13:57 +01:00
djknaeckebrot
43a493bb5a Merge branch 'canary' into feature/delete-docker-volumes 2024-12-23 08:13:12 +01:00
Mauricio Siu
455cae6b8c Merge pull request #938 from 190km/preview-deployment-view
style(preview-deployment): better preview deployment card
2024-12-23 00:40:51 -06:00
Mauricio Siu
869843d9ac Merge pull request #921 from DJKnaeckebrot/feature/swarm-overview
feat: add swarm overview page
2024-12-23 00:09:09 -06:00
Mauricio Siu
d2b662f547 Merge pull request #904 from acgonzales/fix/api-appname
fix: fixed/improved handling of app names in api
2024-12-23 00:08:40 -06:00
Mauricio Siu
31336152ce Merge pull request #968 from drudge/fix-esc-vi
fix: prevent Esc key from closing terminal modals
2024-12-23 00:07:47 -06:00
Mauricio Siu
6afd443257 feat: add swarm overview for servers 2024-12-23 00:03:30 -06:00
Mauricio Siu
1d023ac9f3 Merge pull request #965 from drudge/fancy-ansi
feat(logs): use fancy-ansi for ansi colors
2024-12-22 23:09:02 -06:00
Devin AI
49616e53ea fix: prevent Esc key from closing terminal modals
Co-Authored-By: Nicholas Penree <nick@penree.com>
(cherry picked from commit ee469d5ac3f72cc8e010728cc7c6273c711875b0)
2024-12-22 23:59:57 -05:00
Mauricio Siu
a32e934969 Merge pull request #967 from 190km/white-theme-notifications
style(notifications): fixed white style
2024-12-22 22:59:49 -06:00
190km
eb495b7b99 style: fixed white style 2024-12-23 03:15:11 +01:00
190km
65ddc22010 refactor: removed useless export 2024-12-23 02:41:08 +01:00
190km
7a5b9e3b76 refactor: deleted separate component, and add it to show preview build 2024-12-23 02:34:50 +01:00
Nicholas Penree
5a302d3c47 feat(logs): use fancy-ansi for ansi colors 2024-12-22 15:20:54 -05:00
DJKnaeckebrot
5c5066bc72 chore: remove server from swarms page 2024-12-22 20:07:48 +01:00
UndefinedPony
228d12a61c refactor: code cleanup 2024-12-22 19:01:02 +01:00
UndefinedPony
fd0a9b8b58 feat: add local server config, add local server support in terminal modal, add fixed terminal container height 2024-12-22 18:46:02 +01:00
UndefinedPony
1ea7d2e1bf feat: enable server translations for servers page 2024-12-22 18:44:38 +01:00
UndefinedPony
7e08c8881e refactor: adapt terminal component to pass local server data and add initialized check 2024-12-22 16:37:26 +01:00
UndefinedPony
e68d867d31 refactor: adapt terminal wss to allow local server connection, add status logs 2024-12-22 16:35:28 +01:00
UndefinedPony
a53929a787 feat: add enter terminal option to server dropdown 2024-12-22 16:32:20 +01:00
UndefinedPony
ec8eaf6249 feat: add util to configure and get private ssh key for local ssh connection 2024-12-22 16:31:52 +01:00
UndefinedPony
0d58935ad1 refactor: translations for common terminal stuff 2024-12-22 16:30:53 +01:00
Mauricio Siu
f7c8324c4b Merge pull request #963 from Dokploy/540-need-more-control-over-docker-compose-command-in-deployments
540 need more control over docker compose command in deployments
2024-12-22 01:55:49 -06:00
Mauricio Siu
c0e9670daf refactor: delete log 2024-12-22 01:53:30 -06:00
Mauricio Siu
a710728e77 feat: add support for custom command 2024-12-22 01:51:05 -06:00
João Gabriel
80bd80b786 refactor: make decoration prop optional at discord tester schema 2024-12-22 01:08:34 -03:00
João Gabriel
6e262cde0d fix: update discord notification decoration 2024-12-22 01:07:29 -03:00
João Gabriel
21b2cce7a0 fix: send decoration prop to notification tester 2024-12-22 01:06:47 -03:00
João Gabriel
870e074825 feat: implement decoration to embeds 2024-12-22 00:44:25 -03:00
João Gabriel
055b59e6fa feat: discord decoration switch 2024-12-22 00:42:00 -03:00
João Gabriel
8c06296503 feat: add decoration column to discord notification 2024-12-22 00:39:18 -03:00
190km
29ce8908ee refactor: remove all the props except id, serverid, isloading, and delete function 2024-12-21 23:11:55 +01:00
Mauricio Siu
839e1c0f9f Merge pull request #961 from Dokploy/canary
v0.15.1
2024-12-21 15:42:19 -06:00
Mauricio Siu
54dd531a26 Merge branch 'main' into canary 2024-12-21 15:36:20 -06:00
Mauricio Siu
7ebf5ad0f9 chore: bump version 2024-12-21 15:28:27 -06:00
Mauricio Siu
b85163d935 refactor: add conditional value 2024-12-21 15:10:00 -06:00
Mauricio Siu
a953e59327 Merge pull request #960 from Dokploy/fix/add-latest-cases
refactor: update digest
2024-12-21 15:00:51 -06:00
Mauricio Siu
b2661e4533 refactor: update 2024-12-21 14:58:58 -06:00
Mauricio Siu
883459624e refactor: update digest 2024-12-21 14:54:14 -06:00
Mauricio Siu
6e2b2d564b refactor: add missing additional ports 2024-12-21 14:11:06 -06:00
Mauricio Siu
065963857c Merge pull request #958 from Dokploy/fix/update-fetch
Fix/update fetch
2024-12-21 13:50:13 -06:00
Mauricio Siu
a0c9df4bd4 refactor: add 7 minutes interval 2024-12-21 13:35:17 -06:00
Mauricio Siu
68c8c70260 refactor: iterate pages 2024-12-21 13:34:50 -06:00
Mauricio Siu
a926f28d30 Merge pull request #930 from lost-end-found/additional-ports-management
feat(ports): implement additional ports management
2024-12-21 13:30:32 -06:00
Mauricio Siu
59c0636fb0 refactor: add is cloud to fn 2024-12-21 13:19:30 -06:00
Mauricio Siu
ae159c5678 refactor: add tag to web server update 2024-12-21 13:13:38 -06:00
Mauricio Siu
0abf62dd52 refactor: prevent update appName in services 2024-12-21 13:10:34 -06:00
UndefinedPony
e2b155280a feat: add enter terminal translation 2024-12-21 20:08:08 +01:00
Mauricio Siu
e42e9bec17 refactor: use schema inline 2024-12-21 12:48:32 -06:00
Mauricio Siu
978324e2bf Merge pull request #957 from drudge/check-for-updates
New Updates UI
2024-12-21 12:47:24 -06:00
Nicholas Penree
8f05f06259 Merge remote-tracking branch 'dokploy/canary' into check-for-updates 2024-12-21 13:47:01 -05:00
Mauricio Siu
392be2cfa2 Merge pull request #951 from szwabodev/checkUpdatesTweaks
feat: automatic check for updates
2024-12-21 12:45:39 -06:00
Mauricio Siu
18e89df9a5 Update apps/dokploy/components/layouts/navbar.tsx 2024-12-21 12:45:14 -06:00
UndefinedPony
4d2a9f8aa7 refactor: use dynamic tag for comparing latest tag digest 2024-12-21 13:35:39 -05:00
Nicholas Penree
d08530d451 feat(updates): clean up light mode 2024-12-21 13:02:58 -05:00
UndefinedPony
6c9b12cee9 refactor: use dynamic tag for comparing latest tag digest 2024-12-21 18:33:22 +01:00
Nicholas Penree
a8ff6c7b3f feat(updates): new update UI 2024-12-21 11:24:19 -05:00
UndefinedPony
8699e024ee refactor: add try catch, add default update data 2024-12-21 10:05:31 +01:00
Mauricio Siu
73782ffd26 Merge pull request #954 from Dokploy/915-daily-docker-cleanup-seems-to-be-doing-nothing
fix: add missing notifications in cron jobs
2024-12-21 02:51:52 -06:00
Mauricio Siu
7a8bb8f71d fix: add missing notifications in cron jobs 2024-12-21 02:45:58 -06:00
UndefinedPony
18eae9f7d7 refactor: use service image sha instead of image itself for checking updates 2024-12-21 09:04:25 +01:00
Mauricio Siu
1aae523a0b refactor: add missing verifyToken 2024-12-21 01:53:39 -06:00
UndefinedPony
f40e802331 fix: pull latest release in case of no image when checking update 2024-12-21 08:47:21 +01:00
Mauricio Siu
d979aa17c2 Merge pull request #948 from 190km/notifications-style
style(notifications): better notification item style
2024-12-20 23:34:57 -06:00
Mauricio Siu
e2d20fb0e3 Merge pull request #929 from drudge/certificate-details
feat(certs): show expiration and chain details
2024-12-20 23:23:33 -06:00
Mauricio Siu
62f59c1f9a Merge pull request #941 from mezotv/bump-plausible
feat(plausible): bump to 2.1.4
2024-12-20 23:17:22 -06:00
Mauricio Siu
93e1071057 Merge pull request #949 from 190km/fix-edit-password
fix(settings/profile): edit profile password fixed
2024-12-20 23:17:07 -06:00
Mauricio Siu
788771c5eb refactor: add password in validation 2024-12-20 23:16:38 -06:00
UndefinedPony
ab9aa56c48 refactor: disable automatic updates for cloud version 2024-12-20 18:57:28 +01:00
UndefinedPony
4565b3d7a2 refactor: add latestVersion information to update data 2024-12-20 18:26:54 +01:00
UndefinedPony
c8514e3a1b refactor: remove unused async 2024-12-20 17:32:10 +01:00
UndefinedPony
a06dd17aa1 feat(navbar): add automatic update checking interval, add update available button 2024-12-20 17:30:14 +01:00
UndefinedPony
256534570b refactor: add image tag helper, refactor update check logic, remove try/catch 2024-12-20 17:29:01 +01:00
UndefinedPony
2804748118 refactor: rename action, move pull to updateServer 2024-12-20 17:27:51 +01:00
UndefinedPony
e6bc40e7fe refactor: adapt to navbar version, move confirm action, add reload info 2024-12-20 17:27:14 +01:00
UndefinedPony
196603126b refactor: move check updates function, use new api 2024-12-20 17:26:31 +01:00
UndefinedPony
a5cd8f18cd feat: show auto check update toggle 2024-12-20 17:23:02 +01:00
UndefinedPony
b842887bc3 feat: add toggle for auto updates checking 2024-12-20 16:43:05 +01:00
UndefinedPony
dd64b06340 style: format with biome 2024-12-20 14:09:05 +01:00
UndefinedPony
d9a1976cc0 fix: check updates message fixes 2024-12-20 14:01:55 +01:00
190km
fdfa927532 feat(settings/profile): reset password form after validating password change 2024-12-20 01:00:16 +01:00
190km
bf2551b0f6 fix(settings/profile): fixed password changing 2024-12-20 00:54:32 +01:00
usopp
ed8be62ff3 chore: lint 2024-12-20 00:24:44 +01:00
usopp
77336a21f9 chore: lint 2024-12-20 00:21:51 +01:00
usopp
e05d01788f style(notifications): better notification item style 2024-12-20 00:11:48 +01:00
Dominik Koch
651e81ce6d feat(plausible): bump to 2.1.4 2024-12-19 11:22:58 +01:00
Mauricio Siu
fac29b70a5 Merge pull request #937 from drudge/fix-custom-registry
fix(docker): fix for custom registry login
2024-12-19 02:14:34 -06:00
Mauricio Siu
95eaab43df Merge pull request #936 from drudge/fix-light-term
fix(term): fix light mode foreground color
2024-12-19 02:14:26 -06:00
Mauricio Siu
abdef13b93 refactor: set current color 2024-12-19 02:14:06 -06:00
Mauricio Siu
65f397e1b1 Merge pull request #939 from 190km/2fa-typo-login
fix(2fa-login): typo - Setup -> Login
2024-12-19 02:08:26 -06:00
Mauricio Siu
1ae96297e8 refactor: update lint 2024-12-19 02:07:08 -06:00
Mauricio Siu
c51b502116 refactor: add path join to prevent concatenate double slash and update the getImageName 2024-12-19 02:05:30 -06:00
Mauricio Siu
5a42b78098 Merge pull request #940 from drudge/wrap-secrets
fix(preview-deployments): wrap long envs
2024-12-19 01:19:01 -06:00
Nicholas Penree
b39c0ef915 fix(preview-deployments): wrap long envs 2024-12-18 23:00:50 -05:00
Nicholas Penree
844d582147 fix(docker): fix for custom registry login 2024-12-18 21:58:22 -05:00
190km
0b51088489 fix: typo - Setup -> Login 2024-12-19 02:10:07 +01:00
usopp
9d1cf3736b chore: lint 2024-12-19 01:58:18 +01:00
usopp
3a95474662 chore: lint 2024-12-19 01:44:20 +01:00
190km
3858205e52 style: better preview deployment card 2024-12-18 22:36:57 +01:00
Nicholas Penree
1dece58cff fix(term): fix light mode foreground color
closes #907
2024-12-18 13:56:09 -05:00
Shadow
06b8c82484 feat: add a toggle for replica sets to be used or not 2024-12-18 11:50:30 -06:00
DJKnaeckebrot
8ea453f444 feat: add application handling 2024-12-18 13:01:09 +01:00
djknaeckebrot
63c0912849 xMerge branch 'canary' into feature/delete-docker-volumes 2024-12-18 12:10:04 +01:00
faytranevozter
6211a19805 feat: add support for viewing docker logs in swarm mode 2024-12-18 16:40:33 +07:00
Larry Ioannidis
d22330f983 feat(ports): implement additional ports management 2024-12-18 09:39:20 +00:00
djknaeckebrot
8642d8235e chore: add seperator and make tittles big 2024-12-18 09:14:10 +01:00
djknaeckebrot
b52f57cb0d chore: remove imports 2024-12-18 08:51:49 +01:00
djknaeckebrot
d4d74d3831 refactor: remove not needed import, move to better folder structure 2024-12-18 08:49:02 +01:00
djknaeckebrot
9d497142db feat: add latest cards 2024-12-18 08:43:07 +01:00
Mauricio Siu
852895c382 Merge pull request #912 from drudge/new-ansi-logs
feat(logs): support ansi codes
2024-12-17 23:55:12 -06:00
Mauricio Siu
20d5913820 Merge pull request #910 from drudge/canary
feat(cluster): use code editor for node config
2024-12-17 23:52:25 -06:00
Mauricio Siu
f1b4a73158 Merge pull request #928 from drudge/discord-notif
feat(discord): remove dots
2024-12-17 23:51:01 -06:00
Mauricio Siu
3830f6c4ee Merge pull request #925 from mohabgabber/canary
Added onedev docker compose
2024-12-17 23:50:33 -06:00
Mauricio Siu
5c8eda2405 Update apps/dokploy/templates/onedev/docker-compose.yml 2024-12-17 23:47:48 -06:00
Mauricio Siu
6bf85bcfa3 Merge branch 'canary' into new-ansi-logs 2024-12-17 23:43:18 -06:00
Mauricio Siu
bc03e718bf Merge pull request #911 from 190km/mutlti-select-fiter-logs
feat(logs): multi select fiter logs & hide/show timestamp
2024-12-17 23:42:50 -06:00
Nicholas Penree
a941efb1ff feat(certs): show expiration and chain details 2024-12-17 23:17:29 -05:00
Nicholas Penree
fe2de6b899 feat(discord): remove dots 2024-12-17 22:03:58 -05:00
djknaeckebrot
752c9f2818 style: remove bg and border 2024-12-17 21:35:32 +01:00
djknaeckebrot
577b126e66 feat: make bg transparent 2024-12-17 21:19:38 +01:00
djknaeckebrot
be237ae4cf chore: comment out refresh for now 2024-12-17 21:07:47 +01:00
djknaeckebrot
3080926a50 feat: add new items 2024-12-17 21:07:30 +01:00
djknaeckebrot
e3ee89104b chore: remove tables and add new cards 2024-12-17 20:48:56 +01:00
djknaeckebrot
f98f18b331 feat: add monitoring card 2024-12-17 20:48:35 +01:00
Shadow
8505236263 feat: mongo replica sets 2024-12-17 12:59:18 -06:00
usopp
b3313cf975 style: better white style 2024-12-17 19:16:40 +01:00
Mohab Gabber
4e31d8ac02 Added onedev to templates.ts and onedev's icon 2024-12-17 18:51:38 +02:00
Mohab Gabber
536507377d Added onedev docker compose 2024-12-17 17:23:26 +02:00
djknaeckebrot
763219e859 refactor: streamline imports and improve code formatting in ShowSwarmNodes component 2024-12-17 12:12:48 +01:00
djknaeckebrot
3fc5bfc5c5 feat: implement fetching of Docker service applications and their details 2024-12-17 12:11:43 +01:00
djknaeckebrot
813da8f811 refactor: clean up code formatting and improve readability in swarm dashboard 2024-12-17 12:06:08 +01:00
djknaeckebrot
5716954665 feat: add components for displaying swarm node details and applications 2024-12-17 12:05:39 +01:00
djknaeckebrot
04d3eb9ec0 feat: add swarm tab and dashboard page for managing Docker swarm 2024-12-17 12:05:02 +01:00
djknaeckebrot
b592a025e4 feat: add swarm router and related Docker service functions 2024-12-17 12:04:49 +01:00
Nicholas Penree
6db9c99080 feat(logs): add number of lines filter 2024-12-16 20:12:05 -05:00
Nicholas Penree
7e8953ff44 chore: lint 2024-12-16 17:18:11 -05:00
Nicholas Penree
81c85ce155 fix: don't trigger if already selected 2024-12-16 17:12:18 -05:00
Nicholas Penree
bd16e03602 chore: lint 2024-12-16 17:07:31 -05:00
190km
87a5ce2053 fix: timestamp width 2024-12-16 22:55:36 +01:00
Nicholas Penree
ca4820940e feat(logs): filter improvements 2024-12-16 16:55:13 -05:00
190km
71fe6de9cb feat(logs): added show/hide timestamp option 2024-12-16 21:27:32 +01:00
Nicholas Penree
9ff4968e61 feat(logs): support ansi codes 2024-12-16 14:59:35 -05:00
190km
2312ae1c12 fix: fixed lint 2024-12-16 19:56:47 +01:00
190km
b03011a94f feat(logs): replaced the log type component with the new 2024-12-16 19:50:13 +01:00
190km
7577e40b25 feat(logs): added filter log type component 2024-12-16 19:49:09 +01:00
Nicholas Penree
75e34285ef feat(cluster): use code editor for node config 2024-12-16 11:51:17 -05:00
Aaron Gonzales
8e5b0988cf fix: fixed/improved handling of app names in api 2024-12-16 18:13:07 +08:00
DJKnaeckebrot
7725b3ca36 Merge branch 'Dokploy:canary' into feature/delete-docker-volumes 2024-12-15 19:42:34 +01:00
djknaeckebrot
ef89d05077 Merge branch 'canary' into feature/delete-docker-volumes 2024-12-14 11:07:08 +01:00
djknaeckebrot
92c2a83d92 feat: add filter to delete volumes is wanted 2024-12-13 16:34:46 +01:00
djknaeckebrot
3decbd5207 feat: add new form validator 2024-12-13 16:34:29 +01:00
djknaeckebrot
8779c67b71 refactor: change import names 2024-12-13 16:34:12 +01:00
djknaeckebrot
4dc7d9e3c8 feat: add params to mutation 2024-12-13 16:33:54 +01:00
djknaeckebrot
a439286e5f feat: add UI selection 2024-12-13 16:14:52 +01:00
331 changed files with 15085 additions and 2113 deletions

View File

@@ -87,7 +87,7 @@ export const Login2FA = ({ authId }: Props) => {
</span>
</div>
)}
<CardTitle className="text-xl font-bold">2FA Setup</CardTitle>
<CardTitle className="text-xl font-bold">2FA Login</CardTitle>
<FormField
control={form.control}

View File

@@ -259,7 +259,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
refetch();
})
.catch(() => {
toast.error("Error to update the swarm settings");
toast.error("Error updating the swarm settings");
});
};
return (

View File

@@ -94,7 +94,7 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
});
})
.catch(() => {
toast.error("Error to update the command");
toast.error("Error updating the command");
});
};

View File

@@ -71,7 +71,7 @@ export const AddCommand = ({ applicationId }: Props) => {
});
})
.catch(() => {
toast.error("Error to update the command");
toast.error("Error updating the command");
});
};

View File

@@ -86,7 +86,7 @@ export const AddPort = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the port");
toast.error("Error creating the port");
});
};

View File

@@ -47,10 +47,10 @@ export const DeletePort = ({ portId }: Props) => {
applicationId: data?.applicationId,
});
toast.success("Port delete succesfully");
toast.success("Port delete successfully");
})
.catch(() => {
toast.error("Error to delete the port");
toast.error("Error deleting the port");
});
}}
>

View File

@@ -93,7 +93,7 @@ export const UpdatePort = ({ portId }: Props) => {
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the port");
toast.error("Error updating the port");
});
};

View File

@@ -124,7 +124,7 @@ export const AddRedirect = ({
onDialogToggle(false);
})
.catch(() => {
toast.error("Error to create the redirect");
toast.error("Error creating the redirect");
});
};

View File

@@ -50,10 +50,10 @@ export const DeleteRedirect = ({ redirectId }: Props) => {
utils.application.readTraefikConfig.invalidate({
applicationId: data?.applicationId,
});
toast.success("Redirect delete succesfully");
toast.success("Redirect delete successfully");
})
.catch(() => {
toast.error("Error to delete the redirect");
toast.error("Error deleting the redirect");
});
}}
>

View File

@@ -88,7 +88,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the redirect");
toast.error("Error updating the redirect");
});
};

View File

@@ -75,7 +75,7 @@ export const AddSecurity = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the security");
toast.error("Error creating security");
});
};

View File

@@ -50,10 +50,10 @@ export const DeleteSecurity = ({ securityId }: Props) => {
utils.application.readTraefikConfig.invalidate({
applicationId: data?.applicationId,
});
toast.success("Security delete succesfully");
toast.success("Security delete successfully");
})
.catch(() => {
toast.error("Error to delete the security");
toast.error("Error deleting the security");
});
}}
>

View File

@@ -83,7 +83,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the security");
toast.error("Error updating the security");
});
};

View File

@@ -22,6 +22,13 @@ import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { InfoIcon } from "lucide-react";
const addResourcesApplication = z.object({
memoryReservation: z.number().nullable().optional(),
@@ -72,7 +79,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
toast.error("Error updating the resources");
});
};
return (
@@ -101,10 +108,25 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
name="memoryReservation"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Reservation</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="256 MB"
placeholder="268435456 (256MB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
@@ -120,7 +142,6 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
@@ -132,10 +153,25 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Memory Limit</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>Memory Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"1024 MB"}
placeholder="1073741824 (1GB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
@@ -163,21 +199,36 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Limit</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>CPU Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"2"}
placeholder="2000000000 (2 CPUs)"
{...field}
type="number"
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (
value === "" ||
/^[0-9]*\.?[0-9]*$/.test(value)
) {
const float = Number.parseFloat(value);
field.onChange(float);
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>
@@ -193,21 +244,36 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Reservation</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>CPU Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"1"}
placeholder="1000000000 (1 CPU)"
{...field}
type="number"
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (
value === "" ||
/^[0-9]*\.?[0-9]*$/.test(value)
) {
const float = Number.parseFloat(value);
field.onChange(float);
if (value === "") {
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
field.onChange(number);
}
}
}}
/>

View File

@@ -105,7 +105,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
form.reset();
})
.catch(() => {
toast.error("Error to update the traefik config");
toast.error("Error updating the Traefik config");
});
};

View File

@@ -107,7 +107,7 @@ export const AddVolumes = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the Bind mount");
toast.error("Error creating the Bind mount");
});
} else if (data.type === "volume") {
await mutateAsync({
@@ -122,7 +122,7 @@ export const AddVolumes = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the Volume mount");
toast.error("Error creating the Volume mount");
});
} else if (data.type === "file") {
await mutateAsync({
@@ -138,7 +138,7 @@ export const AddVolumes = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create the File mount");
toast.error("Error creating the File mount");
});
}

View File

@@ -45,10 +45,10 @@ export const DeleteVolume = ({ mountId, refetch }: Props) => {
})
.then(() => {
refetch();
toast.success("Mount deleted succesfully");
toast.success("Mount deleted successfully");
})
.catch(() => {
toast.error("Error to delete the mount");
toast.error("Error deleting the mount");
});
}}
>

View File

@@ -139,7 +139,7 @@ export const UpdateVolume = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the Bind mount");
toast.error("Error updating the Bind mount");
});
} else if (data.type === "volume") {
await mutateAsync({
@@ -153,7 +153,7 @@ export const UpdateVolume = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the Volume mount");
toast.error("Error updating the Volume mount");
});
} else if (data.type === "file") {
await mutateAsync({
@@ -168,7 +168,7 @@ export const UpdateVolume = ({
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the File mount");
toast.error("Error updating the File mount");
});
}
refetch();

View File

@@ -126,7 +126,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the build type");
toast.error("Error saving the build type");
});
};

View File

@@ -47,7 +47,7 @@ export const RefreshToken = ({ applicationId }: Props) => {
toast.success("Refresh updated");
})
.catch(() => {
toast.error("Error to update the refresh token");
toast.error("Error updating the refresh token");
});
}}
>

View File

@@ -1,3 +1,5 @@
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -5,11 +7,10 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { TerminalLine } from "../../docker/logs/terminal-line";
import { LogLine, parseLogs } from "../../docker/logs/utils";
import { Badge } from "@/components/ui/badge";
import { Loader2 } from "lucide-react";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
interface Props {
logPath: string | null;
@@ -19,26 +20,26 @@ interface Props {
}
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
const [data, setData] = useState("");
const [showExtraLogs, setShowExtraLogs] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
const [autoScroll, setAutoScroll] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
};
};
const handleScroll = () => {
if (!scrollRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setAutoScroll(isAtBottom);
};
};
useEffect(() => {
if (!open || !logPath) return;
@@ -69,20 +70,34 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
};
}, [logPath, open]);
useEffect(() => {
const logs = parseLogs(data);
setFilteredLogs(logs);
}, [data]);
let filteredLogsResult = logs;
if (serverId) {
let hideSubsequentLogs = false;
filteredLogsResult = logs.filter((log) => {
if (
log.message.includes(
"===================================EXTRA LOGS============================================",
)
) {
hideSubsequentLogs = true;
return showExtraLogs;
}
return showExtraLogs ? true : !hideSubsequentLogs;
});
}
setFilteredLogs(filteredLogsResult);
}, [data, showExtraLogs]);
useEffect(() => {
scrollToBottom();
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [filteredLogs, autoScroll]);
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [filteredLogs, autoScroll]);
return (
<Dialog
@@ -103,28 +118,49 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
<DialogHeader>
<DialogTitle>Deployment</DialogTitle>
<DialogDescription>
See all the details of this deployment | <Badge variant="blank" className="text-xs">{filteredLogs.length} lines</Badge>
<DialogDescription className="flex items-center gap-2">
<span>
See all the details of this deployment |{" "}
<Badge variant="blank" className="text-xs">
{filteredLogs.length} lines
</Badge>
</span>
{serverId && (
<div className="flex items-center space-x-2">
<Checkbox
id="show-extra-logs"
checked={showExtraLogs}
onCheckedChange={(checked) =>
setShowExtraLogs(checked as boolean)
}
/>
<label
htmlFor="show-extra-logs"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Show Extra Logs
</label>
</div>
)}
</DialogDescription>
</DialogHeader>
<div
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
> {
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => (
<TerminalLine
key={index}
log={log}
noTimestamp
/>
)) :
(
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
>
{" "}
{filteredLogs.length > 0 ? (
filteredLogs.map((log: LogLine, index: number) => (
<TerminalLine key={index} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
</div>
</DialogContent>
</Dialog>

View File

@@ -105,8 +105,8 @@ export const AddDomain = ({
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error to update the domain"
: "Error to create the domain",
? "Error updating the domain"
: "Error creating the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
@@ -264,21 +264,21 @@ export const AddDomain = ({
name="certificateType"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Certificate</FormLabel>
<FormLabel>Certificate Provider</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
<SelectValue placeholder="Select a certificate provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
Let's Encrypt
</SelectItem>
</SelectContent>
</Select>

View File

@@ -57,10 +57,10 @@ export const DeleteDomain = ({ domainId }: Props) => {
});
}
toast.success("Domain delete succesfully");
toast.success("Domain delete successfully");
})
.catch(() => {
toast.error("Error to delete Domain");
toast.error("Error deleting the Domain");
});
}}
>

View File

@@ -51,7 +51,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to add environment");
toast.error("Error adding environment");
});
};

View File

@@ -51,7 +51,7 @@ export const DeployApplication = ({ applicationId }: Props) => {
applicationId,
})
.then(async () => {
toast.success("Application deployed succesfully");
toast.success("Application deployed successfully");
await refetch();
router.push(
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
@@ -59,7 +59,7 @@ export const DeployApplication = ({ applicationId }: Props) => {
})
.catch(() => {
toast.error("Error to deploy Application");
toast.error("Error deploying the Application");
});
await refetch();

View File

@@ -137,7 +137,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the Bitbucket provider");
toast.error("Error saving the Bitbucket provider");
});
};

View File

@@ -68,7 +68,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the Docker provider");
toast.error("Error saving the Docker provider");
});
};

View File

@@ -56,7 +56,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the deployment");
toast.error("Error saving the deployment");
});
};

View File

@@ -84,7 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the Git provider");
toast.error("Error saving the Git provider");
});
};

View File

@@ -131,7 +131,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the github provider");
toast.error("Error saving the github provider");
});
};

View File

@@ -144,7 +144,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the gitlab provider");
toast.error("Error saving the gitlab provider");
});
};

View File

@@ -56,7 +56,7 @@ export const ResetApplication = ({ applicationId, appName }: Props) => {
toast.success("Service Reloaded");
})
.catch(() => {
toast.error("Error to reload the service");
toast.error("Error reloading the service");
});
await refetch();
}}

View File

@@ -69,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
toast.error("Error updating Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
@@ -15,6 +16,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
@@ -29,28 +31,67 @@ export const DockerLogs = dynamic(
},
);
export const badgeStateColor = (state: string) => {
switch (state) {
case "running":
return "green";
case "exited":
case "shutdown":
return "red";
case "accepted":
case "created":
return "blue";
default:
return "default";
}
};
interface Props {
appName: string;
serverId?: string;
}
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
},
);
const [containerId, setContainerId] = useState<string | undefined>();
const [option, setOption] = useState<"swarm" | "native">("native");
const { data: services, isLoading: servicesLoading } =
api.docker.getServiceContainersByAppName.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName && option === "swarm",
},
);
const { data: containers, isLoading: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName && option === "native",
},
);
useEffect(() => {
if (data && data?.length > 0) {
setContainerId(data[0]?.containerId);
if (option === "native") {
if (containers && containers?.length > 0) {
setContainerId(containers[0]?.containerId);
}
} else {
if (services && services?.length > 0) {
setContainerId(services[0]?.containerId);
}
}
}, [data]);
}, [option, services, containers]);
const isLoading = option === "native" ? containersLoading : servicesLoading;
const containersLenght =
option === "native" ? containers?.length : services?.length;
return (
<Card className="bg-background">
@@ -62,7 +103,21 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Label>Select a container to view logs</Label>
<div className="flex flex-row justify-between items-center gap-2">
<Label>Select a container to view logs</Label>
<div className="flex flex-row gap-2 items-center">
<span className="text-sm text-muted-foreground">
{option === "native" ? "Native" : "Swarm"}
</span>
<Switch
checked={option === "native"}
onCheckedChange={(checked) => {
setOption(checked ? "native" : "swarm");
}}
/>
</div>
</div>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{isLoading ? (
@@ -76,21 +131,45 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((container) => (
<SelectItem
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}) {container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
{option === "native" ? (
<div>
{containers?.map((container) => (
<SelectItem
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}){" "}
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem>
))}
</div>
) : (
<>
{services?.map((container) => (
<SelectItem
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}@{container.node}
)
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem>
))}
</>
)}
<SelectLabel>Containers ({containersLenght})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<DockerLogs
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType={option}
/>
</CardContent>
</Card>

View File

@@ -105,8 +105,8 @@ export const AddPreviewDomain = ({
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error to update the domain"
: "Error to create the domain",
? "Error updating the domain"
: "Error creating the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
@@ -265,21 +265,21 @@ export const AddPreviewDomain = ({
name="certificateType"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Certificate</FormLabel>
<FormLabel>Certificate Provider</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
<SelectValue placeholder="Select a certificate provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
Let's Encrypt
</SelectItem>
</SelectContent>
</Select>

View File

@@ -18,15 +18,26 @@ import { ShowDeployment } from "../deployments/show-deployment";
interface Props {
deployments: RouterOutputs["deployment"]["all"];
serverId?: string;
trigger?: React.ReactNode;
}
export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
export const ShowPreviewBuilds = ({
deployments,
serverId,
trigger,
}: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">View Builds</Button>
{trigger ? (
trigger
) : (
<Button className="sm:w-auto w-full" size="sm" variant="outline">
View Builds
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
<DialogHeader>

View File

@@ -1,5 +1,8 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -8,30 +11,34 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { Pencil, RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import {
ExternalLink,
FileText,
GitPullRequest,
Layers,
PenSquare,
RocketIcon,
Trash2,
} from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { ShowDeployment } from "../deployments/show-deployment";
import Link from "next/link";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { DialogAction } from "@/components/shared/dialog-action";
import { AddPreviewDomain } from "./add-preview-domain";
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { ShowPreviewSettings } from "./show-preview-settings";
import { ShowPreviewBuilds } from "./show-preview-builds";
import { ShowPreviewSettings } from "./show-preview-settings";
interface Props {
applicationId: string;
}
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const { data } = api.application.one.useQuery({ applicationId });
const { mutateAsync: deletePreviewDeployment, isLoading } =
api.previewDeployment.delete.useMutation();
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
api.previewDeployment.all.useQuery(
{ applicationId },
@@ -39,10 +46,19 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
enabled: !!applicationId,
},
);
// const [url, setUrl] = React.useState("");
// useEffect(() => {
// setUrl(document.location.origin);
// }, []);
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
deletePreviewDeployment({
previewDeploymentId: previewDeploymentId,
})
.then(() => {
refetchPreviewDeployments();
toast.success("Preview deployment deleted");
})
.catch((error) => {
toast.error(error.message);
});
};
return (
<Card className="bg-background">
@@ -65,7 +81,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
each pull request you create.
</span>
</div>
{data?.previewDeployments?.length === 0 ? (
{!previewDeployments?.length ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<RocketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
@@ -74,120 +90,131 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
</div>
) : (
<div className="flex flex-col gap-4">
{previewDeployments?.map((previewDeployment) => {
const { deployments, domain } = previewDeployment;
{previewDeployments?.map((deployment) => {
const deploymentUrl = `${deployment.domain?.https ? "https" : "http"}://${deployment.domain?.host}${deployment.domain?.path || "/"}`;
const status = deployment.previewStatus;
return (
<div
key={previewDeployment?.previewDeploymentId}
className="flex flex-col justify-between rounded-lg border p-4 gap-2"
key={deployment.previewDeploymentId}
className="group relative overflow-hidden border rounded-lg transition-colors"
>
<div className="flex justify-between gap-2 max-sm:flex-wrap">
<div className="flex flex-col gap-2">
{deployments?.length === 0 ? (
<div>
<span className="text-sm text-muted-foreground">
No deployments found
</span>
</div>
) : (
<div className="flex items-center gap-2">
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{previewDeployment?.pullRequestTitle}
</span>
<StatusTooltip
status={previewDeployment.previewStatus}
className="size-2.5"
/>
</div>
)}
<div className="flex flex-col gap-1">
{previewDeployment?.pullRequestTitle && (
<div className="flex items-center gap-2">
<span className="break-all text-sm text-muted-foreground w-fit">
Title: {previewDeployment?.pullRequestTitle}
</span>
</div>
)}
<div
className={`absolute left-0 top-0 w-1 h-full ${
status === "done"
? "bg-green-500"
: status === "running"
? "bg-yellow-500"
: "bg-red-500"
}`}
/>
{previewDeployment?.pullRequestURL && (
<div className="flex items-center gap-2">
<GithubIcon />
<Link
target="_blank"
href={previewDeployment?.pullRequestURL}
className="break-all text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
>
Pull Request URL
</Link>
<div className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-start gap-3">
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" />
<div>
<div className="font-medium text-sm">
{deployment.pullRequestTitle}
</div>
<div className="text-sm text-muted-foreground mt-1">
{deployment.branch}
</div>
)}
</div>
<div className="flex flex-col ">
<span>Domain </span>
<div className="flex flex-row items-center gap-4">
<Link
target="_blank"
href={`http://${domain?.host}`}
className="text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
>
{domain?.host}
</Link>
<AddPreviewDomain
previewDeploymentId={
previewDeployment.previewDeploymentId
}
domainId={domain?.domainId}
>
<Button variant="outline" size="sm">
<Pencil className="size-4 text-muted-foreground" />
</Button>
</AddPreviewDomain>
</div>
</div>
<Badge variant="outline" className="gap-2">
<StatusTooltip
status={deployment.previewStatus}
className="size-2"
/>
<DateTooltip date={deployment.createdAt} />
</Badge>
</div>
<div className="flex flex-col sm:items-end gap-2 max-sm:w-full">
{previewDeployment?.createdAt && (
<div className="text-sm capitalize text-muted-foreground">
<DateTooltip
date={previewDeployment?.createdAt}
/>
</div>
)}
<ShowPreviewBuilds
deployments={previewDeployment?.deployments || []}
serverId={data?.serverId || ""}
/>
<div className="pl-8 space-y-3">
<div className="relative flex-grow">
<Input
value={deploymentUrl}
readOnly
className="pr-8 text-sm text-blue-500 hover:text-blue-600 cursor-pointer"
onClick={() =>
window.open(deploymentUrl, "_blank")
}
/>
<ExternalLink className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" />
</div>
<ShowModalLogs
appName={previewDeployment.appName}
serverId={data?.serverId || ""}
>
<Button variant="outline">View Logs</Button>
</ShowModalLogs>
<DialogAction
title="Delete Preview"
description="Are you sure you want to delete this preview?"
onClick={() => {
deletePreviewDeployment({
previewDeploymentId:
previewDeployment.previewDeploymentId,
})
.then(() => {
refetchPreviewDeployments();
toast.success("Preview deployment deleted");
})
.catch((error) => {
toast.error(error.message);
});
}}
>
<Button variant="destructive" isLoading={isLoading}>
Delete Preview
<div className="flex gap-2 opacity-80 group-hover:opacity-100 transition-opacity">
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={() =>
window.open(deployment.pullRequestURL, "_blank")
}
>
<GithubIcon className="size-4" />
Pull Request
</Button>
</DialogAction>
<ShowModalLogs
appName={deployment.appName}
serverId={data?.serverId || ""}
>
<Button
variant="outline"
size="sm"
className="gap-2"
>
<FileText className="size-4" />
Logs
</Button>
</ShowModalLogs>
<ShowPreviewBuilds
deployments={deployment.deployments || []}
serverId={data?.serverId || ""}
trigger={
<Button
variant="outline"
size="sm"
className="gap-2"
>
<Layers className="size-4" />
Builds
</Button>
}
/>
<AddPreviewDomain
previewDeploymentId={`${deployment.previewDeploymentId}`}
domainId={deployment.domain?.domainId}
>
<Button
variant="ghost"
size="sm"
className="gap-2"
>
<PenSquare className="size-4" />
</Button>
</AddPreviewDomain>
<DialogAction
title="Delete Preview"
description="Are you sure you want to delete this preview?"
onClick={() =>
handleDeletePreviewDeployment(
deployment.previewDeploymentId,
)
}
>
<Button
variant="ghost"
size="sm"
isLoading={isLoading}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="size-4" />
</Button>
</DialogAction>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,3 @@
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -20,12 +18,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input, NumberInput } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Secrets } from "@/components/ui/secrets";
import { toast } from "sonner";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
@@ -33,6 +26,14 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Settings2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const schema = z.object({
env: z.string(),
@@ -116,7 +117,10 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
<div>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">View Settings</Button>
<Button variant="outline">
<Settings2 className="size-4" />
Configure
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
<DialogHeader>
@@ -218,21 +222,21 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
name="previewCertificateType"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate</FormLabel>
<FormLabel>Certificate Provider</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
<SelectValue placeholder="Select a certificate provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
Let's Encrypt
</SelectItem>
</SelectContent>
</Select>

View File

@@ -63,7 +63,7 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
});
})
.catch(() => {
toast.error("Error to rebuild the application");
toast.error("Error rebuilding the application");
});
}}
>

View File

@@ -49,10 +49,10 @@ export const StartApplication = ({ applicationId }: Props) => {
await utils.application.one.invalidate({
applicationId,
});
toast.success("Application started succesfully");
toast.success("Application started successfully");
})
.catch(() => {
toast.error("Error to start the Application");
toast.error("Error starting the Application");
});
}}
>

View File

@@ -49,10 +49,10 @@ export const StopApplication = ({ applicationId }: Props) => {
await utils.application.one.invalidate({
applicationId,
});
toast.success("Application stopped succesfully");
toast.success("Application stopped successfully");
})
.catch(() => {
toast.error("Error to stop the Application");
toast.error("Error stopping the Application");
});
}}
>

View File

@@ -76,14 +76,14 @@ export const UpdateApplication = ({ applicationId }: Props) => {
description: formData.description || "",
})
.then(() => {
toast.success("Application updated succesfully");
toast.success("Application updated successfully");
utils.application.one.invalidate({
applicationId: applicationId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the application");
toast.error("Error updating the Application");
})
.finally(() => {});
};

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -81,7 +82,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
});
})
.catch(() => {
toast.error("Error to update the command");
toast.error("Error updating the command");
});
};
@@ -91,7 +92,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
<div>
<CardTitle className="text-xl">Run Command</CardTitle>
<CardDescription>
Append a custom command to the compose file
Override a custom command to the compose file
</CardDescription>
</div>
</CardHeader>
@@ -101,6 +102,12 @@ export const AddCommandCompose = ({ composeId }: Props) => {
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<AlertBlock type="warning">
Modifying the default command may affect deployment stability,
impacting logs and monitoring. Proceed carefully and test
thoroughly. By default, the command starts with{" "}
<strong>docker</strong>.
</AlertBlock>
<div className="flex flex-col gap-4">
<FormField
control={form.control}

View File

@@ -1,5 +1,6 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -12,6 +13,7 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -32,6 +34,7 @@ const deleteComposeSchema = z.object({
projectName: z.string().min(1, {
message: "Compose name is required",
}),
deleteVolumes: z.boolean(),
});
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
@@ -51,6 +54,7 @@ export const DeleteCompose = ({ composeId }: Props) => {
const form = useForm<DeleteCompose>({
defaultValues: {
projectName: "",
deleteVolumes: false,
},
resolver: zodResolver(deleteComposeSchema),
});
@@ -58,7 +62,8 @@ export const DeleteCompose = ({ composeId }: Props) => {
const onSubmit = async (formData: DeleteCompose) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ composeId })
const { deleteVolumes } = formData;
await mutateAsync({ composeId, deleteVolumes })
.then((result) => {
push(`/dashboard/project/${result?.projectId}`);
toast.success("Compose deleted successfully");
@@ -133,6 +138,27 @@ export const DeleteCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="deleteVolumes"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="ml-2">
Delete volumes associated with this compose
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>

View File

@@ -47,7 +47,7 @@ export const RefreshTokenCompose = ({ composeId }: Props) => {
toast.success("Refresh Token updated");
})
.catch(() => {
toast.error("Error to update the refresh token");
toast.error("Error updating the refresh token");
});
}}
>

View File

@@ -1,3 +1,5 @@
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -5,12 +7,10 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { TerminalLine } from "../../docker/logs/terminal-line";
import { LogLine, parseLogs } from "../../docker/logs/utils";
import { Badge } from "@/components/ui/badge";
import { Loader2 } from "lucide-react";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
interface Props {
logPath: string | null;
@@ -26,25 +26,25 @@ export const ShowDeploymentCompose = ({
}: Props) => {
const [data, setData] = useState("");
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [showExtraLogs, setShowExtraLogs] = useState(false);
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
const [autoScroll, setAutoScroll] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
};
};
const handleScroll = () => {
if (!scrollRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setAutoScroll(isAtBottom);
};
};
useEffect(() => {
if (!open || !logPath) return;
@@ -76,19 +76,34 @@ export const ShowDeploymentCompose = ({
};
}, [logPath, open]);
useEffect(() => {
const logs = parseLogs(data);
setFilteredLogs(logs);
}, [data]);
let filteredLogsResult = logs;
if (serverId) {
let hideSubsequentLogs = false;
filteredLogsResult = logs.filter((log) => {
if (
log.message.includes(
"===================================EXTRA LOGS============================================",
)
) {
hideSubsequentLogs = true;
return showExtraLogs;
}
return showExtraLogs ? true : !hideSubsequentLogs;
});
}
setFilteredLogs(filteredLogsResult);
}, [data, showExtraLogs]);
useEffect(() => {
scrollToBottom();
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [filteredLogs, autoScroll]);
}, [filteredLogs, autoScroll]);
return (
<Dialog
@@ -109,32 +124,47 @@ export const ShowDeploymentCompose = ({
<DialogContent className={"sm:max-w-5xl max-h-screen"}>
<DialogHeader>
<DialogTitle>Deployment</DialogTitle>
<DialogDescription>
See all the details of this deployment | <Badge variant="blank" className="text-xs">{filteredLogs.length} lines</Badge>
<DialogDescription className="flex items-center gap-2">
<span>
See all the details of this deployment |{" "}
<Badge variant="blank" className="text-xs">
{filteredLogs.length} lines
</Badge>
</span>
{serverId && (
<div className="flex items-center space-x-2">
<Checkbox
id="show-extra-logs"
checked={showExtraLogs}
onCheckedChange={(checked) =>
setShowExtraLogs(checked as boolean)
}
/>
<label
htmlFor="show-extra-logs"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Show Extra Logs
</label>
</div>
)}
</DialogDescription>
</DialogHeader>
<div
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
>
{
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => (
<TerminalLine
key={index}
log={log}
noTimestamp
/>
)) :
(
{filteredLogs.length > 0 ? (
filteredLogs.map((log: LogLine, index: number) => (
<TerminalLine key={index} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)
}
)}
</div>
</DialogContent>
</Dialog>

View File

@@ -127,8 +127,8 @@ export const AddDomainCompose = ({
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error to update the domain"
: "Error to create the domain",
? "Error updating the domain"
: "Error creating the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
@@ -400,21 +400,21 @@ export const AddDomainCompose = ({
name="certificateType"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Certificate</FormLabel>
<FormLabel>Certificate Provider</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
<SelectValue placeholder="Select a certificate provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
Let's Encrypt
</SelectItem>
</SelectContent>
</Select>

View File

@@ -70,7 +70,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to add environment");
toast.error("Error adding environment");
});
};

View File

@@ -83,7 +83,7 @@ export const ComposeActions = ({ composeId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
toast.error("Error updating Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"

View File

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

View File

@@ -53,11 +53,11 @@ export const DeployCompose = ({ composeId }: Props) => {
})
.then(async () => {
router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
);
})
.catch(() => {
toast.error("Error to deploy Compose");
toast.error("Error deploying Compose");
});
await refetch();

View File

@@ -139,7 +139,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the Bitbucket provider");
toast.error("Error saving the Bitbucket provider");
});
};

View File

@@ -83,7 +83,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the Git provider");
toast.error("Error saving the Git provider");
});
};

View File

@@ -133,7 +133,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the github provider");
toast.error("Error saving the Github provider");
});
};

View File

@@ -146,7 +146,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the gitlab provider");
toast.error("Error saving the Gitlab provider");
});
};

View File

@@ -90,7 +90,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
toast.success("Compose updated");
})
.catch(() => {
toast.error("Error to randomize the compose");
toast.error("Error randomizing the compose");
});
};
@@ -105,7 +105,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
toast.success("Compose randomized");
})
.catch(() => {
toast.error("Error to randomize the compose");
toast.error("Error randomizing the compose");
});
};

View File

@@ -62,7 +62,7 @@ export const RedbuildCompose = ({ composeId }: Props) => {
});
})
.catch(() => {
toast.error("Error to rebuild the compose");
toast.error("Error rebuilding the compose");
});
}}
>

View File

@@ -73,7 +73,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error to fetch source type", {
toast.error("Error fetching source type", {
description: err.message,
});
});

View File

@@ -53,10 +53,10 @@ export const StopCompose = ({ composeId }: Props) => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose stopped succesfully");
toast.success("Compose stopped successfully");
})
.catch(() => {
toast.error("Error to stop the compose");
toast.error("Error stopping the compose");
});
}}
>

View File

@@ -0,0 +1,165 @@
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
import { Badge } from "@/components/ui/badge";
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 { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
export const DockerLogs = dynamic(
() =>
import("@/components/dashboard/docker/logs/docker-logs-id").then(
(e) => e.DockerLogsId,
),
{
ssr: false,
},
);
interface Props {
appName: string;
serverId?: string;
}
badgeStateColor;
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
const [option, setOption] = useState<"swarm" | "native">("native");
const [containerId, setContainerId] = useState<string | undefined>();
const { data: services, isLoading: servicesLoading } =
api.docker.getStackContainersByAppName.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName && option === "swarm",
},
);
const { data: containers, isLoading: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
appType: "stack",
serverId,
},
{
enabled: !!appName && option === "native",
},
);
useEffect(() => {
if (option === "native") {
if (containers && containers?.length > 0) {
setContainerId(containers[0]?.containerId);
}
} else {
if (services && services?.length > 0) {
setContainerId(services[0]?.containerId);
}
}
}, [option, services, containers]);
const isLoading = option === "native" ? containersLoading : servicesLoading;
const containersLenght =
option === "native" ? containers?.length : services?.length;
return (
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Logs</CardTitle>
<CardDescription>
Watch the logs of the application in real time
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-row justify-between items-center gap-2">
<Label>Select a container to view logs</Label>
<div className="flex flex-row gap-2 items-center">
<span className="text-sm text-muted-foreground">
{option === "native" ? "Native" : "Swarm"}
</span>
<Switch
checked={option === "native"}
onCheckedChange={(checked) => {
setOption(checked ? "native" : "swarm");
}}
/>
</div>
</div>
<Select onValueChange={setContainerId} value={containerId}>
<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>
{option === "native" ? (
<div>
{containers?.map((container) => (
<SelectItem
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}){" "}
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem>
))}
</div>
) : (
<>
{services?.map((container) => (
<SelectItem
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}@{container.node}
)
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem>
))}
</>
)}
<SelectLabel>Containers ({containersLenght})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<DockerLogs
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType={option}
/>
</CardContent>
</Card>
);
};

View File

@@ -1,3 +1,5 @@
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
@@ -87,7 +89,10 @@ export const ShowDockerLogsCompose = ({
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}) {container.state}
{container.name} ({container.containerId}){" "}
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
@@ -97,6 +102,7 @@ export const ShowDockerLogsCompose = ({
<DockerLogs
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType="native"
/>
</CardContent>
</Card>

View File

@@ -49,10 +49,10 @@ export const StartCompose = ({ composeId }: Props) => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose started succesfully");
toast.success("Compose started successfully");
})
.catch(() => {
toast.error("Error to start the Compose");
toast.error("Error starting the Compose");
});
}}
>

View File

@@ -49,10 +49,10 @@ export const StopCompose = ({ composeId }: Props) => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose stopped succesfully");
toast.success("Compose stopped successfully");
})
.catch(() => {
toast.error("Error to stop the Compose");
toast.error("Error stopping the Compose");
});
}}
>

View File

@@ -76,14 +76,14 @@ export const UpdateCompose = ({ composeId }: Props) => {
description: formData.description || "",
})
.then(() => {
toast.success("Compose updated succesfully");
toast.success("Compose updated successfully");
utils.compose.one.invalidate({
composeId: composeId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the Compose");
toast.error("Error updating the Compose");
})
.finally(() => {});
};

View File

@@ -125,7 +125,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
refetch();
})
.catch(() => {
toast.error("Error to create a backup");
toast.error("Error creating a backup");
});
};
return (

View File

@@ -46,10 +46,10 @@ export const DeleteBackup = ({ backupId, refetch }: Props) => {
.then(() => {
refetch();
toast.success("Backup delete succesfully");
toast.success("Backup deleted successfully");
})
.catch(() => {
toast.error("Error to delete the backup");
toast.error("Error deleting the backup");
});
}}
>

View File

@@ -109,7 +109,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the backup");
toast.error("Error updating the Backup");
});
};

View File

@@ -1,309 +1,297 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Download as DownloadIcon, Loader2 } from "lucide-react";
import React, { useEffect, useRef } from "react";
import { LineCountFilter } from "./line-count-filter";
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
import { StatusLogsFilter } from "./status-logs-filter";
import { TerminalLine } from "./terminal-line";
import { type LogLine, getLogType, parseLogs } from "./utils";
interface Props {
containerId: string;
serverId?: string | null;
containerId: string;
serverId?: string | null;
runType: "swarm" | "native";
}
type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
type TypeFilter = "all" | "error" | "warning" | "success" | "info" | "debug";
export const priorities = [
{
label: "Info",
value: "info",
},
{
label: "Success",
value: "success",
},
{
label: "Warning",
value: "warning",
},
{
label: "Debug",
value: "debug",
},
{
label: "Error",
value: "error",
},
];
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId: serverId ?? undefined,
},
{
enabled: !!containerId,
}
);
export const DockerLogsId: React.FC<Props> = ({
containerId,
serverId,
runType,
}) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId: serverId ?? undefined,
},
{
enabled: !!containerId,
},
);
const [rawLogs, setRawLogs] = React.useState("");
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
const [autoScroll, setAutoScroll] = React.useState(true);
const [lines, setLines] = React.useState<number>(100);
const [search, setSearch] = React.useState<string>("");
const [rawLogs, setRawLogs] = React.useState("");
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
const [autoScroll, setAutoScroll] = React.useState(true);
const [lines, setLines] = React.useState<number>(100);
const [search, setSearch] = React.useState<string>("");
const [showTimestamp, setShowTimestamp] = React.useState(true);
const [since, setSince] = React.useState<TimeFilter>("all");
const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
const scrollRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = React.useState(false);
const [since, setSince] = React.useState<TimeFilter>("all");
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
const scrollRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = React.useState(false);
const scrollToBottom = () => {
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
};
const scrollToBottom = () => {
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
};
const handleScroll = () => {
if (!scrollRef.current) return;
const handleScroll = () => {
if (!scrollRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setAutoScroll(isAtBottom);
};
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setAutoScroll(isAtBottom);
};
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value || "");
};
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value || "");
};
const handleLines = (lines: number) => {
setRawLogs("");
setFilteredLogs([]);
setLines(lines);
};
const handleLines = (e: React.ChangeEvent<HTMLInputElement>) => {
setRawLogs("");
setFilteredLogs([]);
setLines(Number(e.target.value) || 1);
};
const handleSince = (value: TimeFilter) => {
setRawLogs("");
setFilteredLogs([]);
setSince(value);
};
const handleSince = (value: TimeFilter) => {
setRawLogs("");
setFilteredLogs([]);
setSince(value);
};
useEffect(() => {
if (!containerId) return;
const handleTypeFilter = (value: TypeFilter) => {
setTypeFilter(value);
};
let isCurrentConnection = true;
let noDataTimeout: NodeJS.Timeout;
setIsLoading(true);
setRawLogs("");
setFilteredLogs([]);
useEffect(() => {
if (!containerId) return;
let isCurrentConnection = true;
let noDataTimeout: NodeJS.Timeout;
setIsLoading(true);
setRawLogs("");
setFilteredLogs([]);
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const params = new globalThis.URLSearchParams({
containerId,
tail: lines.toString(),
since,
search,
runType,
});
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const params = new globalThis.URLSearchParams({
containerId,
tail: lines.toString(),
since,
search,
});
if (serverId) {
params.append("serverId", serverId);
}
if (serverId) {
params.append("serverId", serverId);
}
const wsUrl = `${protocol}//${
window.location.host
}/docker-container-logs?${params.toString()}`;
console.log("Connecting to WebSocket:", wsUrl);
const ws = new WebSocket(wsUrl);
const wsUrl = `${protocol}//${
window.location.host
}/docker-container-logs?${params.toString()}`;
console.log("Connecting to WebSocket:", wsUrl);
const ws = new WebSocket(wsUrl);
const resetNoDataTimeout = () => {
if (noDataTimeout) clearTimeout(noDataTimeout);
noDataTimeout = setTimeout(() => {
if (isCurrentConnection) {
setIsLoading(false);
}
}, 2000); // Wait 2 seconds for data before showing "No logs found"
};
const resetNoDataTimeout = () => {
if (noDataTimeout) clearTimeout(noDataTimeout);
noDataTimeout = setTimeout(() => {
if (isCurrentConnection) {
setIsLoading(false);
}
}, 2000); // Wait 2 seconds for data before showing "No logs found"
};
ws.onopen = () => {
if (!isCurrentConnection) {
ws.close();
return;
}
console.log("WebSocket connected");
resetNoDataTimeout();
};
ws.onopen = () => {
if (!isCurrentConnection) {
ws.close();
return;
}
console.log("WebSocket connected");
resetNoDataTimeout();
};
ws.onmessage = (e) => {
if (!isCurrentConnection) return;
setRawLogs((prev) => prev + e.data);
setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout);
};
ws.onmessage = (e) => {
if (!isCurrentConnection) return;
setRawLogs((prev) => prev + e.data);
setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout);
};
ws.onerror = (error) => {
if (!isCurrentConnection) return;
console.error("WebSocket error:", error);
setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout);
};
ws.onerror = (error) => {
if (!isCurrentConnection) return;
console.error("WebSocket error:", error);
setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout);
};
ws.onclose = (e) => {
if (!isCurrentConnection) return;
console.log("WebSocket closed:", e.reason);
setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout);
};
ws.onclose = (e) => {
if (!isCurrentConnection) return;
console.log("WebSocket closed:", e.reason);
setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout);
};
return () => {
isCurrentConnection = false;
if (noDataTimeout) clearTimeout(noDataTimeout);
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
};
}, [containerId, serverId, lines, search, since]);
return () => {
isCurrentConnection = false;
if (noDataTimeout) clearTimeout(noDataTimeout);
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
};
}, [containerId, serverId, lines, search, since]);
const handleDownload = () => {
const logContent = filteredLogs
.map(
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
`${timestamp?.toISOString() || "No timestamp"} ${message}`,
)
.join("\n");
const handleDownload = () => {
const logContent = filteredLogs
.map(
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
`${timestamp?.toISOString() || "No timestamp"} ${message}`
)
.join("\n");
const blob = new Blob([logContent], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const appName = data.Name.replace("/", "") || "app";
const isoDate = new Date().toISOString();
a.href = url;
a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
.slice(11, 19)
.replace(/:/g, "")}.log.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const blob = new Blob([logContent], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const appName = data.Name.replace("/", "") || "app";
const isoDate = new Date().toISOString();
a.href = url;
a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
.slice(11, 19)
.replace(/:/g, "")}.log.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleFilter = (logs: LogLine[]) => {
return logs.filter((log) => {
const logType = getLogType(log.message).type;
const handleFilter = (logs: LogLine[]) => {
return logs.filter((log) => {
const logType = getLogType(log.message).type;
if (typeFilter.length === 0) {
return true;
}
const matchesType = typeFilter === "all" || logType === typeFilter;
return typeFilter.includes(logType);
});
};
return matchesType;
});
};
useEffect(() => {
setRawLogs("");
setFilteredLogs([]);
}, [containerId]);
useEffect(() => {
setRawLogs("");
setFilteredLogs([]);
}, [containerId]);
useEffect(() => {
const logs = parseLogs(rawLogs);
const filtered = handleFilter(logs);
setFilteredLogs(filtered);
}, [rawLogs, search, lines, since, typeFilter]);
useEffect(() => {
const logs = parseLogs(rawLogs);
const filtered = handleFilter(logs);
setFilteredLogs(filtered);
}, [rawLogs, search, lines, since, typeFilter]);
useEffect(() => {
scrollToBottom();
useEffect(() => {
scrollToBottom();
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [filteredLogs, autoScroll]);
if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [filteredLogs, autoScroll]);
return (
<div className="flex flex-col gap-4">
<div className="rounded-lg overflow-hidden">
<div className="space-y-4">
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
<div className="flex flex-wrap gap-4">
<LineCountFilter value={lines} onValueChange={handleLines} />
return (
<div className="flex flex-col gap-4">
<div className="rounded-lg overflow-hidden">
<div className="space-y-4">
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
<div className="flex flex-wrap gap-4">
<Input
type="text"
placeholder="Number of lines to show"
value={lines}
onChange={handleLines}
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
/>
<SinceLogsFilter
value={since}
onValueChange={handleSince}
showTimestamp={showTimestamp}
onTimestampChange={setShowTimestamp}
/>
<Select value={since} onValueChange={handleSince}>
<SelectTrigger className="sm:w-[180px] w-full h-9">
<SelectValue placeholder="Time filter" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1h">Last hour</SelectItem>
<SelectItem value="6h">Last 6 hours</SelectItem>
<SelectItem value="24h">Last 24 hours</SelectItem>
<SelectItem value="168h">Last 7 days</SelectItem>
<SelectItem value="720h">Last 30 days</SelectItem>
<SelectItem value="all">All time</SelectItem>
</SelectContent>
</Select>
<StatusLogsFilter
value={typeFilter}
setValue={setTypeFilter}
title="Log type"
options={priorities}
/>
<Select value={typeFilter} onValueChange={handleTypeFilter}>
<SelectTrigger className="sm:w-[180px] w-full h-9">
<SelectValue placeholder="Type filter" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">
<Badge variant="blank">All</Badge>
</SelectItem>
<SelectItem value="error">
<Badge variant="red">Error</Badge>
</SelectItem>
<SelectItem value="warning">
<Badge variant="orange">Warning</Badge>
</SelectItem>
<SelectItem value="debug">
<Badge variant="yellow">Debug</Badge>
</SelectItem>
<SelectItem value="success">
<Badge variant="green">Success</Badge>
</SelectItem>
<SelectItem value="info">
<Badge variant="blue">Info</Badge>
</SelectItem>
</SelectContent>
</Select>
<Input
type="search"
placeholder="Search logs..."
value={search}
onChange={handleSearch}
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
/>
</div>
<Input
type="search"
placeholder="Search logs..."
value={search}
onChange={handleSearch}
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
/>
</div>
<Button
variant="outline"
size="sm"
className="h-9"
onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name}
>
<DownloadIcon className="mr-2 h-4 w-4" />
Download logs
</Button>
</div>
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
>
{filteredLogs.length > 0 ? (
filteredLogs.map((filteredLog: LogLine, index: number) => (
<TerminalLine
key={index}
log={filteredLog}
searchTerm={search}
/>
))
) : isLoading ? (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
No logs found
</div>
)}
</div>
</div>
</div>
</div>
);
};
<Button
variant="outline"
size="sm"
className="h-9 sm:w-auto w-full"
onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name}
>
<DownloadIcon className="mr-2 h-4 w-4" />
Download logs
</Button>
</div>
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
>
{filteredLogs.length > 0 ? (
filteredLogs.map((filteredLog: LogLine, index: number) => (
<TerminalLine
key={index}
log={filteredLog}
searchTerm={search}
noTimestamp={!showTimestamp}
/>
))
) : isLoading ? (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
No logs found
</div>
)}
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,173 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { Command as CommandPrimitive } from "cmdk";
import { debounce } from "lodash";
import { CheckIcon, Hash } from "lucide-react";
import React, { useCallback, useRef } from "react";
const lineCountOptions = [
{ label: "100 lines", value: 100 },
{ label: "300 lines", value: 300 },
{ label: "500 lines", value: 500 },
{ label: "1000 lines", value: 1000 },
{ label: "5000 lines", value: 5000 },
] as const;
interface LineCountFilterProps {
value: number;
onValueChange: (value: number) => void;
title?: string;
}
export function LineCountFilter({
value,
onValueChange,
title = "Limit to",
}: LineCountFilterProps) {
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState("");
const pendingValueRef = useRef<number | null>(null);
const isPresetValue = lineCountOptions.some(
(option) => option.value === value,
);
const debouncedValueChange = useCallback(
debounce((numValue: number) => {
if (numValue > 0 && numValue !== value) {
onValueChange(numValue);
pendingValueRef.current = null;
}
}, 500),
[onValueChange, value],
);
const handleInputChange = (input: string) => {
setInputValue(input);
// Extract numbers from input and convert
const numValue = Number.parseInt(input.replace(/[^0-9]/g, ""));
if (!Number.isNaN(numValue)) {
pendingValueRef.current = numValue;
debouncedValueChange(numValue);
}
};
const handleSelect = (selectedValue: string) => {
const preset = lineCountOptions.find((opt) => opt.label === selectedValue);
if (preset) {
if (preset.value !== value) {
onValueChange(preset.value);
}
setInputValue("");
setOpen(false);
return;
}
const numValue = Number.parseInt(selectedValue);
if (
!Number.isNaN(numValue) &&
numValue > 0 &&
numValue !== value &&
numValue !== pendingValueRef.current
) {
onValueChange(numValue);
setInputValue("");
setOpen(false);
}
};
React.useEffect(() => {
return () => {
debouncedValueChange.cancel();
};
}, [debouncedValueChange]);
const displayValue = isPresetValue
? lineCountOptions.find((option) => option.value === value)?.label
: `${value} lines`;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
>
{title}
<Separator orientation="vertical" className="mx-2 h-4" />
<div className="space-x-1 flex">
<Badge variant="blank" className="rounded-sm px-1 font-normal">
{displayValue}
</Badge>
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<CommandPrimitive className="overflow-hidden rounded-md border border-none bg-popover text-popover-foreground">
<div className="flex items-center border-b px-3">
<Hash className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
placeholder="Number of lines"
value={inputValue}
onValueChange={handleInputChange}
className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const numValue = Number.parseInt(
inputValue.replace(/[^0-9]/g, ""),
);
if (
!Number.isNaN(numValue) &&
numValue > 0 &&
numValue !== value &&
numValue !== pendingValueRef.current
) {
handleSelect(inputValue);
}
}
}}
/>
</div>
<CommandPrimitive.List className="max-h-[300px] overflow-y-auto overflow-x-hidden">
<CommandPrimitive.Group className="px-2 py-1.5">
{lineCountOptions.map((option) => {
const isSelected = value === option.value;
return (
<CommandPrimitive.Item
key={option.value}
onSelect={() => handleSelect(option.label)}
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground"
>
<div
className={cn(
"flex h-4 w-4 items-center justify-center rounded-sm border border-primary mr-2",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
<span>{option.label}</span>
</CommandPrimitive.Item>
);
})}
</CommandPrimitive.Group>
</CommandPrimitive.List>
</CommandPrimitive>
</PopoverContent>
</Popover>
);
}
export default LineCountFilter;

View File

@@ -46,7 +46,11 @@ export const ShowDockerModalLogs = ({
<DialogDescription>View the logs for {containerId}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 pt-2.5">
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
<DockerLogsId
containerId={containerId || ""}
serverId={serverId}
runType="native"
/>
</div>
</DialogContent>
</Dialog>

View File

@@ -0,0 +1,58 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import dynamic from "next/dynamic";
import type React from "react";
export const DockerLogsId = dynamic(
() =>
import("@/components/dashboard/docker/logs/docker-logs-id").then(
(e) => e.DockerLogsId,
),
{
ssr: false,
},
);
interface Props {
containerId: string;
children?: React.ReactNode;
serverId?: string | null;
}
export const ShowDockerModalStackLogs = ({
containerId,
children,
serverId,
}: Props) => {
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
{children}
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogHeader>
<DialogTitle>View Logs</DialogTitle>
<DialogDescription>View the logs for {containerId}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 pt-2.5">
<DockerLogsId
containerId={containerId || ""}
serverId={serverId}
runType="swarm"
/>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,125 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { CheckIcon } from "lucide-react";
import React from "react";
export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
const timeRanges: Array<{ label: string; value: TimeFilter }> = [
{
label: "All time",
value: "all",
},
{
label: "Last hour",
value: "1h",
},
{
label: "Last 6 hours",
value: "6h",
},
{
label: "Last 24 hours",
value: "24h",
},
{
label: "Last 7 days",
value: "168h",
},
{
label: "Last 30 days",
value: "720h",
},
] as const;
interface SinceLogsFilterProps {
value: TimeFilter;
onValueChange: (value: TimeFilter) => void;
showTimestamp: boolean;
onTimestampChange: (show: boolean) => void;
title?: string;
}
export function SinceLogsFilter({
value,
onValueChange,
showTimestamp,
onTimestampChange,
title = "Time range",
}: SinceLogsFilterProps) {
const selectedLabel =
timeRanges.find((range) => range.value === value)?.label ??
"Select time range";
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
>
{title}
<Separator orientation="vertical" className="mx-2 h-4" />
<div className="space-x-1 flex">
<Badge variant="blank" className="rounded-sm px-1 font-normal">
{selectedLabel}
</Badge>
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandList>
<CommandGroup>
{timeRanges.map((range) => {
const isSelected = value === range.value;
return (
<CommandItem
key={range.value}
onSelect={() => {
if (!isSelected) {
onValueChange(range.value);
}
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
<span className="text-sm">{range.label}</span>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
<Separator className="my-2" />
<div className="p-2 flex items-center justify-between">
<span className="text-sm">Show timestamps</span>
<Switch checked={showTimestamp} onCheckedChange={onTimestampChange} />
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,170 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { CheckIcon } from "lucide-react";
import type React from "react";
interface StatusLogsFilterProps {
value?: string[];
setValue?: (value: string[]) => void;
title?: string;
options: {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
}[];
}
export function StatusLogsFilter({
value = [],
setValue,
title,
options,
}: StatusLogsFilterProps) {
const selectedValues = new Set(value as string[]);
const allSelected = selectedValues.size === 0;
const getSelectedBadges = () => {
if (allSelected) {
return (
<Badge variant="blank" className="rounded-sm px-1 font-normal">
All
</Badge>
);
}
if (selectedValues.size >= 1) {
const selected = options.find((opt) => selectedValues.has(opt.value));
return (
<>
<Badge
variant={
selected?.value === "success"
? "green"
: selected?.value === "error"
? "red"
: selected?.value === "warning"
? "orange"
: selected?.value === "info"
? "blue"
: selected?.value === "debug"
? "yellow"
: "blank"
}
className="rounded-sm px-1 font-normal"
>
{selected?.label}
</Badge>
{selectedValues.size > 1 && (
<Badge variant="blank" className="rounded-sm px-1 font-normal">
+{selectedValues.size - 1}
</Badge>
)}
</>
);
}
return null;
};
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
>
{title}
<Separator orientation="vertical" className="mx-2 h-4" />
<div className="space-x-1 flex">{getSelectedBadges()}</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandList>
<CommandGroup>
<CommandItem
onSelect={() => {
setValue?.([]); // Empty array means "All"
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
allSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
<Badge variant="blank">All</Badge>
</CommandItem>
{options.map((option) => {
const isSelected = selectedValues.has(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => {
const newValues = new Set(selectedValues);
if (isSelected) {
newValues.delete(option.value);
} else {
newValues.add(option.value);
}
setValue?.(Array.from(newValues));
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<Badge
variant={
option.value === "success"
? "green"
: option.value === "error"
? "red"
: option.value === "warning"
? "orange"
: option.value === "info"
? "blue"
: option.value === "debug"
? "yellow"
: "blank"
}
>
{option.label}
</Badge>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -7,6 +7,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { FancyAnsi } from "fancy-ansi";
import { escapeRegExp } from "lodash";
import React from "react";
import { type LogLine, getLogType } from "./utils";
@@ -17,6 +18,8 @@ interface LogLineProps {
searchTerm?: string;
}
const fancyAnsi = new FancyAnsi();
export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
const { timestamp, message, rawTimestamp } = log;
const { type, variant, color } = getLogType(message);
@@ -33,17 +36,42 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
: "--- No time found ---";
const highlightMessage = (text: string, term: string) => {
if (!term) return text;
if (!term) {
return (
<span
className="transition-colors"
dangerouslySetInnerHTML={{
__html: fancyAnsi.toHtml(text),
}}
/>
);
}
const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi"));
return parts.map((part, index) =>
part.toLowerCase() === term.toLowerCase() ? (
<span key={index} className="bg-yellow-200 dark:bg-yellow-900">
{part}
</span>
) : (
part
),
const htmlContent = fancyAnsi.toHtml(text);
const modifiedContent = htmlContent.replace(
/<span([^>]*)>([^<]*)<\/span>/g,
(match, attrs, content) => {
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
if (!content.match(searchRegex)) return match;
const segments = content.split(searchRegex);
const wrappedSegments = segments
.map((segment: string) =>
segment.toLowerCase() === term.toLowerCase()
? `<span${attrs} class="bg-yellow-200/50 dark:bg-yellow-900/50">${segment}</span>`
: segment,
)
.join("");
return `<span${attrs}>${wrappedSegments}</span>`;
},
);
return (
<span
className="transition-colors"
dangerouslySetInnerHTML={{ __html: modifiedContent }}
/>
);
};
@@ -104,7 +132,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
</Badge>
</div>
<span className="dark:text-gray-200 font-mono text-foreground whitespace-pre-wrap break-all">
{searchTerm ? highlightMessage(message, searchTerm) : message}
{highlightMessage(message, searchTerm || "")}
</span>
</div>
);

View File

@@ -1,5 +1,5 @@
export type LogType = "error" | "warning" | "success" | "info" | "debug";
export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange";
export type LogType = "error" | "warning" | "success" | "info" | "debug";
export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange";
export interface LogLine {
rawTimestamp: string | null;
@@ -138,8 +138,12 @@ export const getLogType = (message: string): LogStyle => {
if (
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) ||
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage)
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(
lowerMessage,
) ||
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(
lowerMessage,
)
) {
return LOG_STYLES.debug;
}

View File

@@ -59,7 +59,10 @@ export const DockerTerminalModal = ({
{children}
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogContent
className="max-h-screen overflow-y-auto sm:max-w-7xl"
onEscapeKeyDown={(event) => event.preventDefault()}
>
<DialogHeader>
<DialogTitle>Docker Terminal</DialogTitle>
<DialogDescription>
@@ -73,7 +76,7 @@ export const DockerTerminalModal = ({
serverId={serverId || ""}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>
<DialogHeader>
<DialogTitle>
Are you sure you want to close the terminal?

View File

@@ -4,6 +4,7 @@ import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AttachAddon } from "@xterm/addon-attach";
import { useTheme } from "next-themes";
interface Props {
id: string;
@@ -18,6 +19,7 @@ export const DockerTerminal: React.FC<Props> = ({
}) => {
const termRef = useRef(null);
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
const { resolvedTheme } = useTheme();
useEffect(() => {
const container = document.getElementById(id);
if (container) {
@@ -28,8 +30,9 @@ export const DockerTerminal: React.FC<Props> = ({
lineHeight: 1.4,
convertEol: true,
theme: {
cursor: "transparent",
cursor: resolvedTheme === "light" ? "#000000" : "transparent",
background: "rgba(0, 0, 0, 0)",
foreground: "currentColor",
},
});
const addonFit = new FitAddon();

View File

@@ -86,7 +86,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
refetch();
})
.catch(() => {
toast.error("Error to update the traefik config");
toast.error("Error updating the Traefik config");
});
};

View File

@@ -65,7 +65,7 @@ export const ShowAdvancedMariadb = ({ mariadbId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
toast.error("Error updating the resources");
});
};
return (

View File

@@ -18,6 +18,13 @@ import {
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import {
TooltipProvider,
TooltipTrigger,
TooltipContent,
Tooltip,
} from "@/components/ui/tooltip";
import { InfoIcon } from "lucide-react";
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -71,7 +78,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
toast.error("Error updating the resources");
});
};
return (
@@ -100,28 +107,40 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
name="memoryReservation"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Reservation</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="256 MB"
placeholder="268435456 (256MB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
@@ -133,21 +152,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Memory Limit</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>Memory Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"1024 MB"}
placeholder="1073741824 (1GB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
@@ -166,21 +198,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Limit</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>CPU Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"2"}
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
@@ -198,21 +243,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Reservation</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>CPU Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"1"}
placeholder="1000000000 (1 CPU)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}

View File

@@ -144,7 +144,7 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
})
.catch(() => {
toast.error(
"Error to Create the manual backup",
"Error creating the manual backup",
);
});
}}

View File

@@ -70,7 +70,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to add environment");
toast.error("Error adding environment");
});
};

View File

@@ -55,12 +55,12 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
await deploy({
mariadbId,
}).catch(() => {
toast.error("Error to deploy Database");
toast.error("Error deploying Database");
});
await refetch();
})
.catch((e) => {
toast.error(e.message || "Error to deploy Database");
toast.error(e.message || "Error deploying Database");
});
}}
>

View File

@@ -55,7 +55,7 @@ export const ResetMariadb = ({ mariadbId, appName }: Props) => {
toast.success("Service Reloaded");
})
.catch(() => {
toast.error("Error to reload the service");
toast.error("Error reloading the service");
});
await refetch();
}}

View File

@@ -72,7 +72,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the external port");
toast.error("Error saving the external port");
});
};

View File

@@ -49,10 +49,10 @@ export const StopMariadb = ({ mariadbId }: Props) => {
await utils.mariadb.one.invalidate({
mariadbId,
});
toast.success("Application stopped succesfully");
toast.success("Application stopped successfully");
})
.catch(() => {
toast.error("Error to stop the Application");
toast.error("Error stopping the Application");
});
}}
>

View File

@@ -49,10 +49,10 @@ export const StartMariadb = ({ mariadbId }: Props) => {
await utils.mariadb.one.invalidate({
mariadbId,
});
toast.success("Database started succesfully");
toast.success("Database started successfully");
})
.catch(() => {
toast.error("Error to start the Database");
toast.error("Error starting the Database");
});
}}
>

View File

@@ -75,13 +75,13 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
description: formData.description || "",
})
.then(() => {
toast.success("MariaDB updated succesfully");
toast.success("MariaDB updated successfully");
utils.mariadb.one.invalidate({
mariadbId: mariadbId,
});
})
.catch(() => {
toast.error("Error to update the Mariadb");
toast.error("Error updating the Mariadb");
})
.finally(() => {});
};

View File

@@ -65,7 +65,7 @@ export const ShowAdvancedMongo = ({ mongoId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
toast.error("Error updating the resources");
});
};
return (

View File

@@ -19,6 +19,13 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect } from "react";
import {
TooltipProvider,
TooltipTrigger,
TooltipContent,
Tooltip,
} from "@/components/ui/tooltip";
import { InfoIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -71,7 +78,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
toast.error("Error updating the resources");
});
};
return (
@@ -89,10 +96,6 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
the changes.
</AlertBlock>
<Form {...form}>
<AlertBlock type="info">
Please remember to click Redeploy after modify the resources to
apply the changes.
</AlertBlock>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
@@ -104,28 +107,40 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
name="memoryReservation"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Reservation</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="256 MB"
placeholder="268435456 (256MB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
@@ -137,21 +152,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Memory Limit</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>Memory Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"1024 MB"}
placeholder="1073741824 (1GB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
@@ -170,21 +198,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Limit</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>CPU Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"2"}
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
@@ -202,21 +243,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Reservation</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>CPU Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"1"}
placeholder="1000000000 (1 CPU)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}

View File

@@ -144,7 +144,7 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
})
.catch(() => {
toast.error(
"Error to Create the manual backup",
"Error creating the manual backup",
);
});
}}

View File

@@ -70,7 +70,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to add environment");
toast.error("Error adding environment");
});
};

View File

@@ -55,12 +55,12 @@ export const DeployMongo = ({ mongoId }: Props) => {
await deploy({
mongoId,
}).catch(() => {
toast.error("Error to deploy Database");
toast.error("Error deploying Database");
});
await refetch();
})
.catch((e) => {
toast.error(e.message || "Error to deploy Database");
toast.error(e.message || "Error deploying Database");
});
}}
>

View File

@@ -55,7 +55,7 @@ export const ResetMongo = ({ mongoId, appName }: Props) => {
toast.success("Service Reloaded");
})
.catch(() => {
toast.error("Error to reload the service");
toast.error("Error reloading the service");
});
await refetch();
}}

View File

@@ -72,7 +72,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to save the external port");
toast.error("Error saving the external port");
});
};

View File

@@ -49,10 +49,10 @@ export const StopMongo = ({ mongoId }: Props) => {
await utils.mongo.one.invalidate({
mongoId,
});
toast.success("Application stopped succesfully");
toast.success("Application stopped successfully");
})
.catch(() => {
toast.error("Error to stop the Application");
toast.error("Error stopping the Application");
});
}}
>

View File

@@ -49,10 +49,10 @@ export const StartMongo = ({ mongoId }: Props) => {
await utils.mongo.one.invalidate({
mongoId,
});
toast.success("Database started succesfully");
toast.success("Database started successfully");
})
.catch(() => {
toast.error("Error to start the Database");
toast.error("Error starting the Database");
});
}}
>

View File

@@ -76,14 +76,14 @@ export const UpdateMongo = ({ mongoId }: Props) => {
description: formData.description || "",
})
.then(() => {
toast.success("Mongo updated succesfully");
toast.success("Mongo updated successfully");
utils.mongo.one.invalidate({
mongoId: mongoId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update mongo database");
toast.error("Error updating mongo database");
})
.finally(() => {});
};

View File

@@ -65,7 +65,7 @@ export const ShowAdvancedMysql = ({ mysqlId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
toast.error("Error updating the resources");
});
};
return (

View File

@@ -20,6 +20,13 @@ import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import {
TooltipProvider,
TooltipTrigger,
TooltipContent,
Tooltip,
} from "@/components/ui/tooltip";
import { InfoIcon } from "lucide-react";
import { toast } from "sonner";
import { z } from "zod";
@@ -71,7 +78,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
toast.error("Error updating the resources");
});
};
return (
@@ -100,28 +107,40 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
name="memoryReservation"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Reservation</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="256 MB"
placeholder="268435456 (256MB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
@@ -133,21 +152,34 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Memory Limit</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>Memory Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"1024 MB"}
placeholder="1073741824 (1GB in bytes)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
@@ -166,21 +198,34 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Limit</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>CPU Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"2"}
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
@@ -198,21 +243,34 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Reservation</FormLabel>
<div className="flex items-center gap-2">
<FormLabel>CPU Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder={"1"}
placeholder="1000000000 (1 CPU)"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}

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