Compare commits

...

168 Commits

Author SHA1 Message Date
Mauricio Siu
a0b550ace9 Merge pull request #2756 from niieani/bb/fix-null
fix: return an empty object if yaml file is empty
2025-10-05 12:10:55 -06:00
Mauricio Siu
7943c90d5d refactor: enhance middleware removal logic in Traefik configuration 2025-10-05 12:07:19 -06:00
Mauricio Siu
fc3fceb858 refactor: improve Traefik middleware configuration handling and validation 2025-10-05 12:04:21 -06:00
Mauricio Siu
1804a7c301 refactor: remove unnecessary middleware checks in Traefik config generation 2025-10-05 11:26:46 -06:00
autofix-ci[bot]
e97046c267 [autofix.ci] apply automated fixes 2025-10-05 17:14:11 +00:00
Bazyli Brzoska
080233a7cd fix: traefik needs middlewares to be empty/valid 2025-10-05 08:06:06 -07:00
Mauricio Siu
be5d65a8e3 Merge pull request #2684 from sueffuenfelf/fix/docker-terminal-dropdown-containers
fix: docker terminal dropdown not showing containers for applications of type "docker-compose"
2025-10-05 00:51:11 -06:00
Mauricio Siu
e934d4f4ce refactor: remove unused badgeStateColor variable in ShowDockerLogsStack component
- Eliminated the unused badgeStateColor variable to clean up the code.
- Improved overall readability and maintainability of the ShowDockerLogsStack component.
2025-10-05 00:48:07 -06:00
Mauricio Siu
586195b5c8 refactor: enhance DockerTerminalModal component for better prop handling
- Removed unnecessary conditional check for containerId in the main dialog open handler.
- Updated Terminal component to ensure serverId and containerId have default values, improving robustness and user experience.
2025-10-05 00:47:50 -06:00
Mauricio Siu
c8320da716 refactor: simplify props destructuring in DockerTerminalModal component
- Updated the props destructuring to directly include `serverId` instead of using a conditional spread operator.
- Improved code readability by streamlining the object structure.
2025-10-05 00:46:27 -06:00
Mauricio Siu
8a9a0e49ce refactor: remove unused state in DockerTerminal component
- Eliminated the `isConnected` state variable as it was not being utilized.
- Cleaned up imports by removing unused `useState` hook.
2025-10-05 00:45:45 -06:00
Mauricio Siu
aadb278e5f refactor: simplify WebSocket connection logic in DockerTerminal component
- Removed redundant checks for containerId before establishing WebSocket connection.
- Streamlined the connection setup and added the AttachAddon directly after the terminal is opened.
- Updated UI text to clarify the connection method.
2025-10-05 00:45:07 -06:00
Mauricio Siu
47a9bd9c86 Merge branch 'canary' into fix/docker-terminal-dropdown-containers 2025-10-05 00:43:48 -06:00
Mauricio Siu
739dc21bc0 Merge pull request #2679 from dennisimoo/custom-profile-picture
feat: add file upload support for custom profile pictures
2025-10-05 00:37:31 -06:00
Mauricio Siu
fa4724d94e Update profile-form.tsx 2025-10-05 00:35:10 -06:00
autofix-ci[bot]
32454bab61 [autofix.ci] apply automated fixes 2025-10-05 06:30:46 +00:00
Mauricio Siu
beb6f38204 Merge pull request #2599 from Harikrishnan1367709/separate-permission-for-deleting-environments-#2594
feat: Add Environment Deletion Permission Control-#2594
2025-10-05 00:26:54 -06:00
Mauricio Siu
3a0549bbd8 chore: update dokploy version to v0.25.5 in package.json 2025-10-05 00:26:37 -06:00
Mauricio Siu
4112ba9b10 refactor: reorganize user permission checks in AdvancedEnvironmentSelector
- Moved the check for user permissions to delete environments to a more logical position in the code.
- Removed redundant API query for environment data, streamlining the component's state management.
2025-10-05 00:25:18 -06:00
Mauricio Siu
fbf57739b3 feat: add canDeleteEnvironments column to member table
- Introduced a new boolean column `canDeleteEnvironments` to the `member` table with a default value of false.
- Updated journal and snapshot metadata files to include the new migration details for this change.
2025-10-05 00:19:56 -06:00
Mauricio Siu
e4f5a1d828 Merge branch 'canary' into separate-permission-for-deleting-environments-#2594 2025-10-05 00:19:01 -06:00
Mauricio Siu
3e09644877 Remove daily_jack_murdock SQL script and associated journal entry from the project. This change eliminates the canDeleteEnvironments column from the member table, streamlining the database schema. 2025-10-05 00:17:31 -06:00
Mauricio Siu
1ab576d260 Merge pull request #2598 from Harikrishnan1367709/separate-permission-for-creating-environments-#2593
feat: Add environment creation permission control-#2593
2025-10-05 00:16:39 -06:00
Mauricio Siu
0b0f507b49 feat: add functionality to create a new environment when a project is created
- Integrated the `addNewEnvironment` function into the project creation process.
- Ensured that the environment is associated with the current user and organization.
2025-10-05 00:15:02 -06:00
Mauricio Siu
fa8722f6c8 feat: add canCreateEnvironments column to member table and update metadata
- Introduced a new boolean column `canCreateEnvironments` to the `member` table with a default value of false.
- Updated journal and snapshot metadata files to include the new migration details.
2025-10-05 00:09:23 -06:00
Mauricio Siu
fb0ed494fc Merge branch 'canary' into separate-permission-for-creating-environments-#2593 2025-10-05 00:08:49 -06:00
Mauricio Siu
6d2728f5f0 chore: remove deprecated SQL migration files for member permissions
- Deleted SQL migration files `0111_magical_nova.sql` and `0112_serious_hellcat.sql` which added `canCreateEnvironments` and `canCreateEnvironmentsInProjects` columns to the `member` table.
- Updated journal and snapshot metadata files to reflect the removal of these migrations.
2025-10-05 00:08:02 -06:00
Mauricio Siu
8efc8b573c Merge pull request #2577 from robgraeber/patch-1
Fix swarm settings config placeholders
2025-10-05 00:04:24 -06:00
Mauricio Siu
644189064b Merge pull request #2232 from perinm/feature/stop-grace-period-2227
feat: Add stop_grace_period to swarm settings
2025-10-05 00:01:44 -06:00
Mauricio Siu
23c891d6fc feat: add stopGracePeriodSwarm column to multiple database tables and update journal and snapshot metadata 2025-10-04 23:57:13 -06:00
Mauricio Siu
a3f9f9b7a1 Merge branch 'canary' into feature/stop-grace-period-2227 2025-10-04 23:45:59 -06:00
Mauricio Siu
83a7b8dce5 refactor: remove stop grace period swarm migrations and snapshots 2025-10-04 23:45:32 -06:00
autofix-ci[bot]
e9b5699f8e [autofix.ci] apply automated fixes 2025-10-05 05:43:58 +00:00
Mauricio Siu
f952f53fca Merge pull request #2678 from dennisimoo/update-logos
style: replace generic icons with Gotify and Ntfy brand logos
2025-10-04 23:43:17 -06:00
autofix-ci[bot]
60db2972c7 [autofix.ci] apply automated fixes 2025-10-05 05:42:41 +00:00
Mauricio Siu
143e4be9e6 Merge pull request #2744 from Captainsalem/canary
fix: correct typo in saveGitProvider function name
2025-10-04 23:36:20 -06:00
Mauricio Siu
18e553f239 Merge pull request #2764 from Dokploy/2530-new-user-email-invitation-does-not-render-correctly-on-osxs-mailapp
chore: update better-auth to version 1.3.26 and adjust dependencies i…
2025-10-04 21:53:39 -06:00
Mauricio Siu
c41f447269 chore: downgrade better-auth to version v1.2.8-beta.7 in package.json files and update dependencies in pnpm-lock.yaml 2025-10-04 21:51:50 -06:00
Mauricio Siu
dbc4f4e4c5 chore: update better-auth to version 1.3.26 and adjust dependencies in package.json files 2025-10-04 21:45:48 -06:00
Mauricio Siu
8594ad8ece Merge pull request #2763 from Dokploy/2645-github-auto-deploy-webhook-responds-404
2645 GitHub auto deploy webhook responds 404
2025-10-04 20:59:01 -06:00
Mauricio Siu
9edd69b10d refactor: remove console log from WebDomain component 2025-10-04 20:58:30 -06:00
Mauricio Siu
4a9684bbe4 refactor: simplify URL change warning in WebDomain component 2025-10-04 20:58:07 -06:00
Mauricio Siu
4f835c6c5e feat: add warning alert for URL changes in WebDomain component 2025-10-04 20:56:38 -06:00
Bazyli Brzoska
54853098a7 fix: return an empty object if yaml file is empty 2025-10-04 17:19:24 -07:00
artemis37
cdca2ea6d2 fix: correct typo in saveGitProvider function name
- Fixed "saveGitProdiver" to "saveGitProvider" in API router
- Updated corresponding component usage to maintain consistency
2025-10-02 02:44:47 +03:00
Mauricio Siu
9f5c2dbe92 chore: update version to v0.25.4 in package.json 2025-09-28 22:32:35 -06:00
Mauricio Siu
0f9505327f Merge pull request #2710 from SimonLoir/canary
fix: add environment in buildLink for docker compose deploy notifications
2025-09-27 15:14:48 -06:00
Simon Loir
dd2902a57c fix: fix buildLink in docker compose deploy notifications 2025-09-27 16:50:25 +02:00
Mauricio Siu
0138a7c011 Merge pull request #2532 from monntterro/feat/gitea-http-support
feat: support cloning repositories over HTTP in Gitea integration
2025-09-27 03:17:08 -06:00
autofix-ci[bot]
845d2a3ac5 [autofix.ci] apply automated fixes 2025-09-27 09:15:31 +00:00
Mauricio Siu
4033bb84b2 Merge pull request #2640 from amirparsadd/patch-1
feat: support Arvancloud CDN detection
2025-09-27 03:14:12 -06:00
Mauricio Siu
43e96edcdd Merge pull request #2668 from alsmadi99/canary
feat(scheduler): auto-switch to 'Custom' on manual input
2025-09-27 03:13:00 -06:00
Mauricio Siu
2db388536f Merge pull request #2700 from dennisimoo/compose-alert
feat: add unsaved changes tracking and UI indication
2025-09-27 03:09:33 -06:00
Mauricio Siu
43876efc79 Merge pull request #2677 from dennisimoo/fix-position
style: move Deployments tab after Domains tab
2025-09-27 03:07:02 -06:00
Mauricio Siu
e7c7545c02 Merge pull request #2706 from Dokploy/2673-bitbucket-deployments-are-broken-auth-token-wont-work
fix(bitbucket): enhance Bitbucket authentication handling
2025-09-27 02:58:49 -06:00
autofix-ci[bot]
77705381cd [autofix.ci] apply automated fixes 2025-09-27 08:56:28 +00:00
Mauricio Siu
5fdf82a27f refactor(bitbucket): remove debug console logs from repository cloning process
- Removed console logs for clone URL and repository information to clean up the output during the cloning process.
2025-09-27 02:55:42 -06:00
Mauricio Siu
6bd5b1f71f fix(bitbucket): enhance Bitbucket authentication handling
- Added support for Bitbucket email and workspace name in the authentication process.
- Updated the clone URL generation to use the correct format for API tokens.
- Improved error handling to ensure required fields are provided for both API tokens and app passwords.
- Added console logs for debugging clone URL and repository information during cloning.
2025-09-27 02:55:06 -06:00
Mauricio Siu
17d6830b66 Merge pull request #2705 from Dokploy/2670-bug-deployments-are-mark-as-running-when-they-never-ended-vps-shutdown
2670 bug deployments are mark as running when they never ended vps shutdown
2025-09-27 02:23:53 -06:00
Mauricio Siu
a845eba320 Merge pull request #2696 from Harikrishnan1367709/Most-services-has-no-effect-#2691
Feat: "Most services" sorting to count total services across environments -2691
2025-09-27 02:22:58 -06:00
Mauricio Siu
2f4ec9f35f fix(deployment): reintroduce deployment cancellation during server initialization
- Added the call to initCancelDeployments back into the server initialization process to ensure that deployment cancellations are handled correctly in all environments.
2025-09-27 02:21:02 -06:00
autofix-ci[bot]
b725861b55 [autofix.ci] apply automated fixes 2025-09-27 08:20:36 +00:00
Mauricio Siu
6fa8f63277 fix(deployment): correct deployment cancellation logic and ensure proper status update
- Updated the initCancelDeployments function to set the status of running deployments to 'cancelled' instead of 'error'.
- Reintroduced the call to initCancelDeployments in the server initialization process to ensure cancellations are handled correctly.
2025-09-27 02:20:07 -06:00
Mauricio Siu
ac6bdf60ec feat(deployment): add 'cancelled' status to deployment and implement cancellation logic
- Updated the deployment status enum to include 'cancelled'.
- Added a new utility function to handle the cancellation of deployments, setting their status to 'error'.
- Enhanced the status tooltip component to display 'Cancelled' when the status is 'cancelled'.
- Created a new SQL migration to add the 'cancelled' value to the deploymentStatus type.
2025-09-27 02:15:43 -06:00
randomperson12344
db292e6949 feat: add unsaved changes tracking and UI indication 2025-09-26 20:13:09 -07:00
montero
085f6bbbb7 refactor(gitea): extract clone URL construction into a reusable function 2025-09-26 22:01:54 +03:00
autofix-ci[bot]
cbdc4e4a20 [autofix.ci] apply automated fixes 2025-09-26 08:48:23 +00:00
HarikrishnanD
ee3ff18feb fix: correct "Most services" sorting to count total services across environments - Fix sorting logic to count actual services instead of environment count - Projects now properly sort by total service count in descending order - Resolves issue where "Most services" showed ascending order instead of descending -#2691 2025-09-26 14:15:58 +05:30
autofix-ci[bot]
598ecb8c6e [autofix.ci] apply automated fixes 2025-09-25 06:39:08 +00:00
Sofien Scholze
1d5a523b9e fix: docker terminal dropdown not showing containers for application of type "docker-compose" 2025-09-24 22:52:20 +02:00
Lucas Manchine
4bced9ede0 fix: db migrations for stop grace period swarm 2025-09-24 12:00:09 -03:00
Lucas Manchine
e35aeef4e2 fix: db migrations for stop grace period swarm 2025-09-24 11:53:02 -03:00
Lucas Manchine
5e89ffbf4f fix: extend-database-schemas-with-stopgraceperiodswarm 2025-09-24 10:50:04 -03:00
Lucas Manchine
21de6bf167 test: add missing test 2025-09-24 10:26:36 -03:00
Lucas Manchine
291edce62f fixing migration 2025-09-24 10:02:15 -03:00
Lucas Manchine
59be1c5941 fix: coerce-stopgraceperiodswarm-to-number 2025-09-24 09:54:54 -03:00
Lucas Manchine
2141e4b174 Merge branch 'canary' into feature/stop-grace-period-2227 2025-09-24 08:52:32 -03:00
randomperson12344
df0fb340ad feat: add file upload support for custom profile pictures 2025-09-23 22:32:32 -07:00
randomperson12344
190ccfa91f style: replace generic icons with Gotify and Ntfy brand logos 2025-09-23 21:04:55 -07:00
randomperson12344
f5084dd5fb feat(ui): move Deployments tab to position 4 after Domains tab 2025-09-23 19:23:43 -07:00
autofix-ci[bot]
1b603d84d7 [autofix.ci] apply automated fixes 2025-09-22 19:11:08 +00:00
Mohammad Alsmadi
cf2c89d136 feat(scheduler): auto-switch to 'Custom' on manual input 2025-09-22 13:35:52 +04:00
Amirparsa Baghdadi
95de98e94d close string 2025-09-22 12:37:21 +03:30
Mauricio Siu
569d43ae7f Merge pull request #2525 from divaltor/bitbucket-api-token
feat(bitbucket): Deprecate App password and replace it with API token
2025-09-21 15:18:40 -06:00
Mauricio Siu
d22ed9b569 refactor(bitbucket): streamline extractCommitedPaths function by passing Bitbucket object directly 2025-09-21 15:15:21 -06:00
Mauricio Siu
8b88c85b37 refactor(bitbucket): simplify extractCommitedPaths function by using Bitbucket type and centralized header generation 2025-09-21 15:14:15 -06:00
Mauricio Siu
11fbd047d0 feat(bitbucket): enhance API token creation instructions in Bitbucket provider settings 2025-09-21 14:13:55 -06:00
Mauricio Siu
69af9c0312 refactor(gitea): update repository and branch fetching to use pagination with /user/repos and /branches endpoints 2025-09-21 14:10:00 -06:00
Mauricio Siu
063d51e442 feat(bitbucket): add bitbucketEmail field to Bitbucket provider settings and update related API and database schema 2025-09-21 13:54:53 -06:00
Mauricio Siu
0a789e1d6f feat(bitbucket): add apiToken column to the Bitbucket table and update migration journal with new entry for 0111_mushy_wolfsbane 2025-09-21 03:10:52 -06:00
Mauricio Siu
671cd497fd Merge branch 'canary' into bitbucket-api-token 2025-09-21 03:10:37 -06:00
Mauricio Siu
8ddc254252 chore: remove '0110_smiling_slapstick' migration and associated journal entry 2025-09-21 03:10:30 -06:00
Mauricio Siu
2668e22302 feat(bitbucket): add apiToken column to Bitbucket table and update migration journal 2025-09-21 03:09:32 -06:00
Mauricio Siu
37145fbdf2 chore: bump version to v0.25.3 in package.json 2025-09-21 03:02:43 -06:00
Mauricio Siu
6847d8dbef Merge pull request #2516 from cheetahbyte/fix/special-characters-passwords
fix(registries): special character passwords not working in registry creation.
2025-09-21 02:45:43 -06:00
Mauricio Siu
032bcb7459 Merge pull request #2657 from Dokploy/2529-renaming-a-git-provider-wont-update-the-external-link-url
feat: add appName field to GitHub provider settings and update relate…
2025-09-21 02:41:11 -06:00
Mauricio Siu
68be7a259f Merge pull request #2656 from Dokploy/2533-unknown-tag-reset-error-in-domains-when-extending-docker-compose-configuration
refactor: replace js-yaml with yaml package for YAML parsing and stri…
2025-09-21 02:40:40 -06:00
Mauricio Siu
7d682870ff feat: add appName field to GitHub provider settings and update related API and database schema 2025-09-21 02:39:20 -06:00
Mauricio Siu
d1a1a80c77 refactor: further standardize YAML parsing in test files by replacing load with parse 2025-09-21 02:38:16 -06:00
Mauricio Siu
3d7dc82232 refactor: update test files to consistently use parse function for YAML parsing 2025-09-21 02:35:36 -06:00
Mauricio Siu
fedc88eb40 refactor: consistently replace load function with parse for YAML parsing in all test files 2025-09-21 02:28:30 -06:00
Mauricio Siu
5d0f6a4657 refactor: replace load function with parse for YAML parsing in test files 2025-09-21 02:27:16 -06:00
Mauricio Siu
4718461405 refactor: update YAML parsing from js-yaml to yaml package in test files 2025-09-21 02:24:41 -06:00
Mauricio Siu
80b22d9458 refactor: replace js-yaml with yaml package for YAML parsing and stringifying across the application 2025-09-21 02:20:20 -06:00
Mauricio Siu
8fa5fe7f2c Merge pull request #2654 from Dokploy/2018-traefik-never-start-error-read-etctraefiktraefikyml-is-a-directory
2018 traefik never start error read etctraefiktraefikyml is a directory
2025-09-21 01:39:06 -06:00
Mauricio Siu
4ced8bec96 feat: add completion message and exit process after Dokploy setup 2025-09-21 01:35:46 -06:00
Mauricio Siu
9ecb770a01 fix: enhance Traefik setup by adding directory checks and cleanup for existing config files 2025-09-21 01:31:21 -06:00
Mauricio Siu
8ac586b2f7 Merge pull request #2653 from Dokploy/2554-ai-assistant-is-broken-in-v025
fix: handle optional configFiles in template details and improve mapp…
2025-09-21 01:11:27 -06:00
Mauricio Siu
0a1800ba6d fix: adjust layout by removing unnecessary flex class from password input container 2025-09-21 01:09:12 -06:00
Mauricio Siu
f13028ee70 fix: handle optional configFiles in template details and improve mapping safety 2025-09-21 01:07:25 -06:00
Mauricio Siu
b6b6b9f2ce Merge pull request #2652 from Dokploy/2630-backups-dont-get-deleted-when-backup-fails
fix: enhance error handling in volume backup process by adding cleanu…
2025-09-21 00:27:45 -06:00
Mauricio Siu
f46637b8e1 fix: enhance error handling in volume backup process by adding cleanup for .tar files 2025-09-21 00:26:48 -06:00
Mauricio Siu
948ed2cc0d fix: improve registry tag construction to conditionally include registry URL 2025-09-21 00:13:56 -06:00
Mauricio Siu
a536c977f0 Merge pull request #2651 from Dokploy/2633-error-parsing-reference-app-aaa-9bkzznlatest-is-not-a-valid-repositorytag-invalid-reference-format
fix: update registry tag construction to handle optional registry URL
2025-09-21 00:11:21 -06:00
Mauricio Siu
8524cd0972 fix: update registry tag construction to handle optional registry URL 2025-09-21 00:09:19 -06:00
Mauricio Siu
ac1e51cd11 Merge pull request #2650 from Dokploy/2638-overlay-network-not-working-across-nodes
refactor: replace getPublicIpWithFallback with getLocalServerIp for i…
2025-09-21 00:01:28 -06:00
Mauricio Siu
ca243d7259 refactor: replace getPublicIpWithFallback with getLocalServerIp for improved local IP retrieval 2025-09-20 23:57:38 -06:00
Mauricio Siu
e1ce54c159 Merge pull request #2622 from Harikrishnan1367709/Compose-does-not-display-the-domain-under-dashboard/projects-#2606
fix: Display Compose service domains in projects dashboard (#2606)
2025-09-20 16:37:49 -06:00
Mauricio Siu
031302d808 Merge pull request #2643 from nimone/patch-1
fix: prevent the shrinking of icon button for view mode on add template dialog
2025-09-20 16:36:57 -06:00
Mauricio Siu
5e01505e4d fix: update input class for better responsiveness in add template component 2025-09-20 16:36:36 -06:00
Mauricio Siu
c423724972 Merge pull request #2614 from Harikrishnan1367709/Profile-email-field-accepts-empty-values-causing-sign-in-issues-#2613
Fix profile email validation to prevent empty values causing sign-in issues-#2613
2025-09-20 16:34:20 -06:00
Mauricio Siu
f1f7639708 Merge pull request #2624 from dragospaulpop/dragospaulpop-patch-cloneRawGitlabRepositoryRemote
Fix: Update gitlab.ts cloneRawGitlabRepositoryRemote to use gitlabBranch
2025-09-20 16:32:19 -06:00
Mauricio Siu
9ef1a76a85 Merge pull request #2582 from yigitahmetsahin/feat/improve-db-backups
feat(backups): make mariadb backups non-blocking
2025-09-20 16:31:16 -06:00
Nishant Mogha
30b66a4828 fix: prevent shrinking icon button for view mode on add template 2025-09-19 21:13:20 +05:30
Amirparsa Baghdadi
4416ca9cd2 Add arvancloud to CDNs 2025-09-19 15:58:22 +03:30
Dragos-Paul Pop
f2ead66890 Update gitlab.ts cloneRawGitlabRepositoryRemote to use gitlabBranch
Cloning a GitLab repository for a compose service to a remote server incorrectly used the "branch" column from Postgres' "compose" table instead of the "gitlabBranch" column causing an error.
2025-09-17 11:48:12 +03:00
HarikrishnanD
64475bbb13 fix: Compose domain display logic in projects dashboard - Uncommented the commented-out Compose domain rendering code in ShowProjects.tsx - Fixed data structure to properly iterate through project.environments and env.compose - Added proper condition checking for compose services - Compose services now display their domains in the projects dashboard dropdown - Resolves issue #2606 where template-deployed Compose services didn't show domains 2025-09-17 14:07:03 +05:30
autofix-ci[bot]
c1896f8877 [autofix.ci] apply automated fixes 2025-09-16 07:47:55 +00:00
HarikrishnanD
d13975adac fix: add email validation to profile form to prevent empty values - Add email format and required validation to profile form schema - Add email validation to API schema and service layer - Improve error handling in user update mutation - Fixes issue where users could save empty email causing sign-in failures -#2613 2025-09-16 13:11:22 +05:30
Mauricio Siu
d9398b9558 feat(workers): add third worker and increase concurrency for existing workers 2025-09-15 23:43:27 -06:00
Mauricio Siu
788dbe4050 chore(package): bump version from v0.25.1 to v0.25.2 2025-09-15 23:23:03 -06:00
Mauricio Siu
6934f44778 Merge pull request #2573 from Harikrishnan1367709/Duplicating-a-service-does-not-refresh-the-list-afterwards-#2565-Harikrishnan
feat: Auto-refresh services list when duplicating to same environment
2025-09-15 23:18:40 -06:00
Mauricio Siu
b8e9602538 feat(bitbucket): update Bitbucket token management and add API token column to database 2025-09-15 23:10:50 -06:00
Mauricio Siu
afca968853 chore: remove unused migration and associated journal entry for '0110_dry_golden_guardian' 2025-09-15 23:03:43 -06:00
Mauricio Siu
457a6db00f Merge pull request #2562 from sundakai/canary
fix:traefik 3.5.0 error
2025-09-15 22:59:11 -06:00
Mauricio Siu
81f89a0796 Merge pull request #2597 from demondayza/canary
fix: fix typo for Github clone
2025-09-15 22:27:00 -06:00
autofix-ci[bot]
65c5974b4f [autofix.ci] apply automated fixes 2025-09-12 13:49:51 +00:00
autofix-ci[bot]
bdf0a932fe [autofix.ci] apply automated fixes 2025-09-12 13:46:33 +00:00
HarikrishnanD
c355eafc95 feat: add environment deletion permission control - Add canDeleteEnvironments field to member table - Implement permission validation in environment deletion endpoint - Add UI toggle in user permissions modal - Hide delete buttons for users without permission - Maintain backward compatibility for owners/admins #2594 2025-09-12 19:09:30 +05:30
Andrew Margetts
d8a98f3936 fix: fix typo for Github clone 2025-09-12 15:27:10 +02:00
HarikrishnanD
30b28afbac feat: add canCreateEnvironments permission for environment creation - Add database field and API validation - Implement permission checking in environment creation - Add UI toggle in user permissions modal - Hide create button for unauthorized users Fixes #2593 2025-09-12 17:56:02 +05:30
Yigit SAHIN
c9715b19a3 feat(backups): make mariadb backups non-blocking closes #2443 2025-09-10 11:27:22 +03:00
Rob Graeber
1a940580ae Fix swarm settings config placeholders 2025-09-09 18:03:02 -06:00
autofix-ci[bot]
ec11325165 [autofix.ci] apply automated fixes 2025-09-09 16:40:00 +00:00
HarikrishnanD
abcbd2d599 feat: auto-refresh services list when duplicating to same environment - Add cache invalidation for environment.one and environment.byProjectId queries - Fix issue where duplicated services weren't visible until hard refresh - Ensure proper invalidation when duplicating to current environment - Resolves #2565 2025-09-09 22:07:40 +05:30
永恒
1664ae9b92 fix traefik 3.5.0 error
fix traefik error:"both Docker and Swarm labels are defined"
2025-09-08 12:26:36 +08:00
Lucas Manchine
b7e2df6d6a refactor: clean up stopGracePeriodSwarm assignment formatting │
│                                                                                                                                                                                                                                                                                                                                                                                                    │
│   - Improve code readability by condensing multi-line assignment                                                                                                                                                                                                                                                                                                                                   │
│   - Maintain consistent formatting with other field assignments                                                                                                                                                                                                                                                                                                                                    │
│   - No functional changes, formatting only
2025-09-05 15:34:03 -03:00
Lucas Manchine
85e3a92877 feat(swarm): add stop grace period configuration for Docker Swarm services
- Add stopGracePeriodSwarm field to application schema for configuring container shutdown grace period
- Update swarm settings UI to include nanosecond input for stop grace period
- Regenerate migration as 0110 to resolve sequence conflict with canary branch
- Clean up commented debug code and reorganize imports

The stop grace period allows users to specify how long Docker should wait before forcefully
terminating a container during shutdown, improving graceful shutdown handling for applications.
2025-09-05 13:23:46 -03:00
Lucas Manchine
c2eaa78724 refactor: clean up stopGracePeriodSwarm implementation │
│                                                                                                                                                                                                                                                                                                                               │
│   - Remove commented debug code                                                                                                                                                                                                                                                                                               │
│   - Reorganize imports for better readability
2025-09-05 13:05:46 -03:00
Lucas Manchine
270b4d4edc Merge branch 'canary' into feature/stop-grace-period-2227-alt 2025-09-05 12:34:17 -03:00
Vlad Vladov
a6ca41f91f feat(bitbucket): Re-generate migration 2025-09-05 18:09:18 +03:00
Vlad Vladov
b2b649c5cd refactor(bitbucket): Extract duplicated code to a function 2025-09-05 18:08:37 +03:00
autofix-ci[bot]
225c398d31 [autofix.ci] apply automated fixes 2025-09-05 18:08:37 +03:00
Vlad Vladov
07b99bd4e4 style(ui): Remove tooltip 2025-09-05 18:08:37 +03:00
autofix-ci[bot]
652e8910f4 [autofix.ci] apply automated fixes 2025-09-05 18:08:36 +03:00
Vlad Vladov
e04e25385d feat(bitbucket): Deprecate App password and replace it with API token 2025-09-05 18:08:35 +03:00
Lucas Manchine
da9df3e239 testing changes 2025-09-05 11:49:32 -03:00
autofix-ci[bot]
68945c6888 [autofix.ci] apply automated fixes 2025-09-03 18:17:04 +00:00
Leonhard Breuer
146d82b6c4 feat: use printf instead of echo 2025-09-03 20:12:16 +02:00
Leonhard Breuer
02215d4e21 fix: use new command for registry updates 2025-09-03 19:59:17 +02:00
Leonhard Breuer
4ca05414af fix: use shellsafe docker command
- add `shEscape` function - add `safeDockerLoginCommand` - use the new
functions to contruct better registry login command
2025-09-03 19:52:01 +02:00
Lucas Manchine
8ea64f9de1 testing changes 2025-08-06 14:55:30 -03:00
Lucas Manchine
825a1fc495 Merge branch 'canary' into feature/stop-grace-period-2227 2025-08-06 10:30:57 -03:00
Mauricio Siu
7b76bb93b3 Merge branch 'canary' into feature/stop-grace-period-2227 2025-08-02 19:37:24 -06:00
Lucas Manchine
64290fcbf6 fix linter issues 2025-07-29 09:33:19 -03:00
Lucas Manchine
4f2b270f1d improved form 2025-07-23 18:32:42 -03:00
Lucas Manchine
e22489926b feat: Add stop_grace_period to swarm settings, test layout 2025-07-23 21:18:57 +00:00
Lucas Manchine
b4a5221caf feat: Add stop_grace_period to swarm settings 2025-07-23 20:38:27 +00:00
127 changed files with 41659 additions and 828 deletions

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllProperties } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile1 = `
version: "3.8"
@@ -61,7 +61,7 @@ secrets:
file: ./db_password.txt
`;
const expectedComposeFile1 = load(`
const expectedComposeFile1 = parse(`
version: "3.8"
services:
@@ -120,7 +120,7 @@ secrets:
`) as ComposeSpecification;
test("Add suffix to all properties in compose file 1", () => {
const composeData = load(composeFile1) as ComposeSpecification;
const composeData = parse(composeFile1) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
@@ -185,7 +185,7 @@ secrets:
file: ./db_password.txt
`;
const expectedComposeFile2 = load(`
const expectedComposeFile2 = parse(`
version: "3.8"
services:
@@ -243,7 +243,7 @@ secrets:
`) as ComposeSpecification;
test("Add suffix to all properties in compose file 2", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
@@ -308,7 +308,7 @@ secrets:
file: ./service_secret.txt
`;
const expectedComposeFile3 = load(`
const expectedComposeFile3 = parse(`
version: "3.8"
services:
@@ -366,7 +366,7 @@ secrets:
`) as ComposeSpecification;
test("Add suffix to all properties in compose file 3", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
@@ -420,7 +420,7 @@ volumes:
driver: local
`;
const expectedComposeFile = load(`
const expectedComposeFile = parse(`
version: "3.8"
services:
@@ -467,7 +467,7 @@ volumes:
`) as ComposeSpecification;
test("Add suffix to all properties in Plausible compose file", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -23,7 +23,7 @@ configs:
`;
test("Add suffix to configs in root property", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -59,7 +59,7 @@ configs:
`;
test("Add suffix to multiple configs in root property", () => {
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification;
const composeData = parse(composeFileMultipleConfigs) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -92,7 +92,7 @@ configs:
`;
test("Add suffix to configs with different properties in root property", () => {
const composeData = load(
const composeData = parse(
composeFileDifferentProperties,
) as ComposeSpecification;
@@ -137,7 +137,7 @@ configs:
`;
// Expected compose file con el prefijo `testhash`
const expectedComposeFileConfigRoot = load(`
const expectedComposeFileConfigRoot = parse(`
version: "3.8"
services:
@@ -162,7 +162,7 @@ configs:
`) as ComposeSpecification;
test("Add suffix to configs in root property", () => {
const composeData = load(composeFileConfigRoot) as ComposeSpecification;
const composeData = parse(composeFileConfigRoot) as ComposeSpecification;
const suffix = "testhash";

View File

@@ -3,8 +3,8 @@ import {
addSuffixToConfigsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = `
version: "3.8"
@@ -22,7 +22,7 @@ configs:
`;
test("Add suffix to configs in services", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -54,7 +54,7 @@ configs:
`;
test("Add suffix to configs in services with single config", () => {
const composeData = load(
const composeData = parse(
composeFileSingleServiceConfig,
) as ComposeSpecification;
@@ -108,7 +108,7 @@ configs:
`;
test("Add suffix to configs in services with multiple configs", () => {
const composeData = load(
const composeData = parse(
composeFileMultipleServicesConfigs,
) as ComposeSpecification;
@@ -157,7 +157,7 @@ services:
`;
// Expected compose file con el prefijo `testhash`
const expectedComposeFileConfigServices = load(`
const expectedComposeFileConfigServices = parse(`
version: "3.8"
services:
@@ -182,7 +182,7 @@ services:
`) as ComposeSpecification;
test("Add suffix to configs in services", () => {
const composeData = load(composeFileConfigServices) as ComposeSpecification;
const composeData = parse(composeFileConfigServices) as ComposeSpecification;
const suffix = "testhash";

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -43,7 +43,7 @@ configs:
file: ./db-config.yml
`;
const expectedComposeFileCombinedConfigs = load(`
const expectedComposeFileCombinedConfigs = parse(`
version: "3.8"
services:
@@ -77,7 +77,7 @@ configs:
`) as ComposeSpecification;
test("Add suffix to all configs in root and services", () => {
const composeData = load(composeFileCombinedConfigs) as ComposeSpecification;
const composeData = parse(composeFileCombinedConfigs) as ComposeSpecification;
const suffix = "testhash";
@@ -122,7 +122,7 @@ configs:
file: ./db-config.yml
`;
const expectedComposeFileWithEnvAndExternal = load(`
const expectedComposeFileWithEnvAndExternal = parse(`
version: "3.8"
services:
@@ -159,7 +159,7 @@ configs:
`) as ComposeSpecification;
test("Add suffix to configs with environment and external", () => {
const composeData = load(
const composeData = parse(
composeFileWithEnvAndExternal,
) as ComposeSpecification;
@@ -200,7 +200,7 @@ configs:
file: ./db-config.yml
`;
const expectedComposeFileWithTemplateDriverAndLabels = load(`
const expectedComposeFileWithTemplateDriverAndLabels = parse(`
version: "3.8"
services:
@@ -231,7 +231,7 @@ configs:
`) as ComposeSpecification;
test("Add suffix to configs with template driver and labels", () => {
const composeData = load(
const composeData = parse(
composeFileWithTemplateDriverAndLabels,
) as ComposeSpecification;

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = `
version: "3.8"
@@ -35,7 +35,7 @@ test("Generate random hash with 8 characters", () => {
});
test("Add suffix to networks root property", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -79,7 +79,7 @@ networks:
`;
test("Add suffix to advanced networks root property (2 TRY)", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -120,7 +120,7 @@ networks:
`;
test("Add suffix to networks with external properties", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -160,7 +160,7 @@ networks:
`;
test("Add suffix to networks with IPAM configurations", () => {
const composeData = load(composeFile4) as ComposeSpecification;
const composeData = parse(composeFile4) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -201,7 +201,7 @@ networks:
`;
test("Add suffix to networks with custom options", () => {
const composeData = load(composeFile5) as ComposeSpecification;
const composeData = parse(composeFile5) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -264,7 +264,7 @@ networks:
`;
test("Add suffix to networks with static suffix", () => {
const composeData = load(composeFile6) as ComposeSpecification;
const composeData = parse(composeFile6) as ComposeSpecification;
const suffix = "testhash";
@@ -273,7 +273,7 @@ test("Add suffix to networks with static suffix", () => {
}
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
const expectedComposeData = load(
const expectedComposeData = parse(
expectedComposeFile6,
) as ComposeSpecification;
expect(networks).toStrictEqual(expectedComposeData.networks);
@@ -293,7 +293,7 @@ networks:
`;
test("It shoudn't add suffix to dokploy-network", () => {
const composeData = load(composeFile7) as ComposeSpecification;
const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -3,8 +3,8 @@ import {
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = `
version: "3.8"
@@ -23,7 +23,7 @@ services:
`;
test("Add suffix to networks in services", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -67,7 +67,7 @@ networks:
`;
test("Add suffix to networks in services with aliases", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -107,7 +107,7 @@ networks:
`;
test("Add suffix to networks in services (Object with simple networks)", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -153,7 +153,7 @@ networks:
`;
test("Add suffix to networks in services (combined case)", () => {
const composeData = load(composeFileCombined) as ComposeSpecification;
const composeData = parse(composeFileCombined) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -196,7 +196,7 @@ services:
`;
test("It shoudn't add suffix to dokploy-network in services", () => {
const composeData = load(composeFile7) as ComposeSpecification;
const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -245,7 +245,7 @@ services:
`;
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
const composeData = load(composeFile8) as ComposeSpecification;
const composeData = parse(composeFile8) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -5,8 +5,8 @@ import {
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileCombined = `
version: "3.8"
@@ -39,7 +39,7 @@ networks:
`;
test("Add suffix to networks in services and root (combined case)", () => {
const composeData = load(composeFileCombined) as ComposeSpecification;
const composeData = parse(composeFileCombined) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -89,7 +89,7 @@ test("Add suffix to networks in services and root (combined case)", () => {
expect(redisNetworks).not.toHaveProperty("backend");
});
const expectedComposeFile = load(`
const expectedComposeFile = parse(`
version: "3.8"
services:
@@ -120,7 +120,7 @@ networks:
`);
test("Add suffix to networks in compose file", () => {
const composeData = load(composeFileCombined) as ComposeSpecification;
const composeData = parse(composeFileCombined) as ComposeSpecification;
const suffix = "testhash";
if (!composeData?.networks) {
@@ -156,7 +156,7 @@ networks:
driver: bridge
`;
const expectedComposeFile2 = load(`
const expectedComposeFile2 = parse(`
version: "3.8"
services:
@@ -182,7 +182,7 @@ networks:
`);
test("Add suffix to networks in compose file with external and internal networks", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
@@ -218,7 +218,7 @@ networks:
com.docker.network.bridge.enable_icc: "true"
`;
const expectedComposeFile3 = load(`
const expectedComposeFile3 = parse(`
version: "3.8"
services:
@@ -247,7 +247,7 @@ networks:
`);
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
@@ -289,7 +289,7 @@ networks:
`;
const expectedComposeFile4 = load(`
const expectedComposeFile4 = parse(`
version: "3.8"
services:
@@ -326,7 +326,7 @@ networks:
`);
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
const composeData = load(composeFile4) as ComposeSpecification;
const composeData = parse(composeFile4) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -23,7 +23,7 @@ secrets:
`;
test("Add suffix to secrets in root property", () => {
const composeData = load(composeFileSecretsRoot) as ComposeSpecification;
const composeData = parse(composeFileSecretsRoot) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData?.secrets) {
@@ -52,7 +52,7 @@ secrets:
`;
test("Add suffix to secrets in root property (Test 1)", () => {
const composeData = load(composeFileSecretsRoot1) as ComposeSpecification;
const composeData = parse(composeFileSecretsRoot1) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData?.secrets) {
@@ -84,7 +84,7 @@ secrets:
`;
test("Add suffix to secrets in root property (Test 2)", () => {
const composeData = load(composeFileSecretsRoot2) as ComposeSpecification;
const composeData = parse(composeFileSecretsRoot2) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData?.secrets) {

View File

@@ -3,8 +3,8 @@ import {
addSuffixToSecretsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileSecretsServices = `
version: "3.8"
@@ -21,7 +21,7 @@ secrets:
`;
test("Add suffix to secrets in services", () => {
const composeData = load(composeFileSecretsServices) as ComposeSpecification;
const composeData = parse(composeFileSecretsServices) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData.services) {
@@ -54,7 +54,9 @@ secrets:
`;
test("Add suffix to secrets in services (Test 1)", () => {
const composeData = load(composeFileSecretsServices1) as ComposeSpecification;
const composeData = parse(
composeFileSecretsServices1,
) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData.services) {
@@ -93,7 +95,9 @@ secrets:
`;
test("Add suffix to secrets in services (Test 2)", () => {
const composeData = load(composeFileSecretsServices2) as ComposeSpecification;
const composeData = parse(
composeFileSecretsServices2,
) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData.services) {

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllSecrets } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileCombinedSecrets = `
version: "3.8"
@@ -25,7 +25,7 @@ secrets:
file: ./app_secret.txt
`;
const expectedComposeFileCombinedSecrets = load(`
const expectedComposeFileCombinedSecrets = parse(`
version: "3.8"
services:
@@ -48,7 +48,7 @@ secrets:
`) as ComposeSpecification;
test("Add suffix to all secrets", () => {
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification;
const composeData = parse(composeFileCombinedSecrets) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
@@ -77,7 +77,7 @@ secrets:
file: ./cache_secret.txt
`;
const expectedComposeFileCombinedSecrets3 = load(`
const expectedComposeFileCombinedSecrets3 = parse(`
version: "3.8"
services:
@@ -99,7 +99,9 @@ secrets:
`) as ComposeSpecification;
test("Add suffix to all secrets (3rd Case)", () => {
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification;
const composeData = parse(
composeFileCombinedSecrets3,
) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
@@ -128,7 +130,7 @@ secrets:
file: ./db_password.txt
`;
const expectedComposeFileCombinedSecrets4 = load(`
const expectedComposeFileCombinedSecrets4 = parse(`
version: "3.8"
services:
@@ -150,7 +152,9 @@ secrets:
`) as ComposeSpecification;
test("Add suffix to all secrets (4th Case)", () => {
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification;
const composeData = parse(
composeFileCombinedSecrets4,
) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = `
version: "3.8"
@@ -27,7 +27,7 @@ test("Generate random hash with 8 characters", () => {
});
test("Add suffix to service names with container_name in compose file", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -32,7 +32,7 @@ networks:
`;
test("Add suffix to service names with depends_on (array) in compose file", () => {
const composeData = load(composeFile4) as ComposeSpecification;
const composeData = parse(composeFile4) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -102,7 +102,7 @@ networks:
`;
test("Add suffix to service names with depends_on (object) in compose file", () => {
const composeData = load(composeFile5) as ComposeSpecification;
const composeData = parse(composeFile5) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -30,7 +30,7 @@ networks:
`;
test("Add suffix to service names with extends (string) in compose file", () => {
const composeData = load(composeFile6) as ComposeSpecification;
const composeData = parse(composeFile6) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -90,7 +90,7 @@ networks:
`;
test("Add suffix to service names with extends (object) in compose file", () => {
const composeData = load(composeFile7) as ComposeSpecification;
const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -31,7 +31,7 @@ networks:
`;
test("Add suffix to service names with links in compose file", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -26,7 +26,7 @@ networks:
`;
test("Add suffix to service names in compose file", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -3,8 +3,8 @@ import {
addSuffixToAllServiceNames,
addSuffixToServiceNames,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileCombinedAllCases = `
version: "3.8"
@@ -38,7 +38,7 @@ networks:
driver: bridge
`;
const expectedComposeFile = load(`
const expectedComposeFile = parse(`
version: "3.8"
services:
@@ -71,7 +71,9 @@ networks:
`);
test("Add suffix to all service names in compose file", () => {
const composeData = load(composeFileCombinedAllCases) as ComposeSpecification;
const composeData = parse(
composeFileCombinedAllCases,
) as ComposeSpecification;
const suffix = "testhash";
@@ -131,7 +133,7 @@ networks:
driver: bridge
`;
const expectedComposeFile1 = load(`
const expectedComposeFile1 = parse(`
version: "3.8"
services:
@@ -176,7 +178,7 @@ networks:
`) as ComposeSpecification;
test("Add suffix to all service names in compose file 1", () => {
const composeData = load(composeFile1) as ComposeSpecification;
const composeData = parse(composeFile1) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
@@ -227,7 +229,7 @@ networks:
driver: bridge
`;
const expectedComposeFile2 = load(`
const expectedComposeFile2 = parse(`
version: "3.8"
services:
@@ -271,7 +273,7 @@ networks:
`) as ComposeSpecification;
test("Add suffix to all service names in compose file 2", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
@@ -322,7 +324,7 @@ networks:
driver: bridge
`;
const expectedComposeFile3 = load(`
const expectedComposeFile3 = parse(`
version: "3.8"
services:
@@ -366,7 +368,7 @@ networks:
`) as ComposeSpecification;
test("Add suffix to all service names in compose file 3", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -35,7 +35,7 @@ networks:
`;
test("Add suffix to service names with volumes_from in compose file", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -4,8 +4,8 @@ import {
addSuffixToVolumesRoot,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = `
services:
@@ -70,7 +70,7 @@ volumes:
driver: local
`;
const expectedDockerCompose = load(`
const expectedDockerCompose = parse(`
services:
mail:
image: bytemark/smtp
@@ -143,7 +143,7 @@ test("Generate random hash with 8 characters", () => {
// Docker compose needs unique names for services, volumes, networks and containers
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
test("Add suffix to volumes root property", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -165,7 +165,7 @@ test("Add suffix to volumes root property", () => {
});
test("Expect to change the suffix in all the possible places", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -195,7 +195,7 @@ volumes:
mongo-data:
`;
const expectedDockerCompose2 = load(`
const expectedDockerCompose2 = parse(`
version: '3.8'
services:
app:
@@ -218,7 +218,7 @@ volumes:
`) as ComposeSpecification;
test("Expect to change the suffix in all the possible places (2 Try)", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -248,7 +248,7 @@ volumes:
mongo-data:
`;
const expectedDockerCompose3 = load(`
const expectedDockerCompose3 = parse(`
version: '3.8'
services:
app:
@@ -271,7 +271,7 @@ volumes:
`) as ComposeSpecification;
test("Expect to change the suffix in all the possible places (3 Try)", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -645,7 +645,7 @@ volumes:
db-config:
`;
const expectedDockerComposeComplex = load(`
const expectedDockerComposeComplex = parse(`
version: "3.8"
services:
studio:
@@ -1012,7 +1012,7 @@ volumes:
`);
test("Expect to change the suffix in all the possible places (4 Try)", () => {
const composeData = load(composeFileComplex) as ComposeSpecification;
const composeData = parse(composeFileComplex) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -1065,7 +1065,7 @@ volumes:
db-data:
`;
const expectedDockerComposeExample1 = load(`
const expectedDockerComposeExample1 = parse(`
version: "3.8"
services:
web:
@@ -1111,7 +1111,7 @@ volumes:
`) as ComposeSpecification;
test("Expect to change the suffix in all the possible places (5 Try)", () => {
const composeData = load(composeFileExample1) as ComposeSpecification;
const composeData = parse(composeFileExample1) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
@@ -1143,7 +1143,7 @@ volumes:
backrest-cache:
`;
const expectedDockerComposeBackrest = load(`
const expectedDockerComposeBackrest = parse(`
services:
backrest:
image: garethgeorge/backrest:v1.7.3
@@ -1168,7 +1168,7 @@ volumes:
`) as ComposeSpecification;
test("Should handle volume paths with subdirectories correctly", () => {
const composeData = load(composeFileBackrest) as ComposeSpecification;
const composeData = parse(composeFileBackrest) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFile = `
version: "3.8"
@@ -29,7 +29,7 @@ test("Generate random hash with 8 characters", () => {
});
test("Add suffix to volumes in root property", () => {
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -67,7 +67,7 @@ networks:
`;
test("Add suffix to volumes in root property (Case 2)", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const composeData = parse(composeFile2) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -101,7 +101,7 @@ networks:
`;
test("Add suffix to volumes in root property (Case 3)", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const composeData = parse(composeFile3) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -148,7 +148,7 @@ volumes:
`;
// Expected compose file con el prefijo `testhash`
const expectedComposeFile4 = load(`
const expectedComposeFile4 = parse(`
version: "3.8"
services:
@@ -179,7 +179,7 @@ volumes:
`) as ComposeSpecification;
test("Add suffix to volumes in root property", () => {
const composeData = load(composeFile4) as ComposeSpecification;
const composeData = parse(composeFile4) as ComposeSpecification;
const suffix = "testhash";

View File

@@ -3,8 +3,8 @@ import {
addSuffixToVolumesInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
test("Generate random hash with 8 characters", () => {
const hash = generateRandomHash();
@@ -24,7 +24,7 @@ services:
`;
test("Add suffix to volumes declared directly in services", () => {
const composeData = load(composeFile1) as ComposeSpecification;
const composeData = parse(composeFile1) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -59,7 +59,7 @@ volumes:
`;
test("Add suffix to volumes declared directly in services (Case 2)", () => {
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -1,7 +1,7 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllVolumes } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
import { parse } from "yaml";
const composeFileTypeVolume = `
version: "3.8"
@@ -23,7 +23,7 @@ volumes:
driver: local
`;
const expectedComposeFileTypeVolume = load(`
const expectedComposeFileTypeVolume = parse(`
version: "3.8"
services:
@@ -44,7 +44,7 @@ volumes:
`) as ComposeSpecification;
test("Add suffix to volumes with type: volume in services", () => {
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
const suffix = "testhash";
@@ -73,7 +73,7 @@ volumes:
driver: local
`;
const expectedComposeFileTypeVolume1 = load(`
const expectedComposeFileTypeVolume1 = parse(`
version: "3.8"
services:
@@ -93,7 +93,7 @@ volumes:
`) as ComposeSpecification;
test("Add suffix to mixed volumes in services", () => {
const composeData = load(composeFileTypeVolume1) as ComposeSpecification;
const composeData = parse(composeFileTypeVolume1) as ComposeSpecification;
const suffix = "testhash";
@@ -128,7 +128,7 @@ volumes:
device: /path/to/app/logs
`;
const expectedComposeFileTypeVolume2 = load(`
const expectedComposeFileTypeVolume2 = parse(`
version: "3.8"
services:
@@ -154,7 +154,7 @@ volumes:
`) as ComposeSpecification;
test("Add suffix to complex volume configurations in services", () => {
const composeData = load(composeFileTypeVolume2) as ComposeSpecification;
const composeData = parse(composeFileTypeVolume2) as ComposeSpecification;
const suffix = "testhash";
@@ -218,7 +218,7 @@ volumes:
device: /path/to/shared/logs
`;
const expectedComposeFileTypeVolume3 = load(`
const expectedComposeFileTypeVolume3 = parse(`
version: "3.8"
services:
@@ -273,7 +273,7 @@ volumes:
`) as ComposeSpecification;
test("Add suffix to complex nested volumes configuration in services", () => {
const composeData = load(composeFileTypeVolume3) as ComposeSpecification;
const composeData = parse(composeFileTypeVolume3) as ComposeSpecification;
const suffix = "testhash";

View File

@@ -133,6 +133,7 @@ const baseApp: ApplicationNested = {
username: null,
dockerContextPath: null,
rollbackActive: false,
stopGracePeriodSwarm: null,
};
describe("unzipDrop using real zip files", () => {

View File

@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ApplicationNested } from "@dokploy/server/utils/builders";
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
type MockCreateServiceOptions = {
StopGracePeriod?: number;
[key: string]: unknown;
};
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
vi.hoisted(() => {
const inspect = vi.fn<[], Promise<never>>();
const getService = vi.fn(() => ({ inspect }));
const createService = vi.fn<[MockCreateServiceOptions], Promise<void>>(
async () => undefined,
);
const getRemoteDocker = vi.fn(async () => ({
getService,
createService,
}));
return {
inspectMock: inspect,
getServiceMock: getService,
createServiceMock: createService,
getRemoteDockerMock: getRemoteDocker,
};
});
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
getRemoteDocker: getRemoteDockerMock,
}));
const createApplication = (
overrides: Partial<ApplicationNested> = {},
): ApplicationNested =>
({
appName: "test-app",
buildType: "dockerfile",
env: null,
mounts: [],
cpuLimit: null,
memoryLimit: null,
memoryReservation: null,
cpuReservation: null,
command: null,
ports: [],
sourceType: "docker",
dockerImage: "example:latest",
registry: null,
environment: {
project: { env: null },
env: null,
},
replicas: 1,
stopGracePeriodSwarm: 0n,
serverId: "server-id",
...overrides,
}) as unknown as ApplicationNested;
describe("mechanizeDockerContainer", () => {
beforeEach(() => {
inspectMock.mockReset();
inspectMock.mockRejectedValue(new Error("service not found"));
getServiceMock.mockClear();
createServiceMock.mockClear();
getRemoteDockerMock.mockClear();
getRemoteDockerMock.mockResolvedValue({
getService: getServiceMock,
createService: createServiceMock,
});
});
it("converts bigint stopGracePeriodSwarm to a number and keeps zero values", async () => {
const application = createApplication({ stopGracePeriodSwarm: 0n });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings.StopGracePeriod).toBe(0);
expect(typeof settings.StopGracePeriod).toBe("number");
});
it("omits StopGracePeriod when stopGracePeriodSwarm is null", async () => {
const application = createApplication({ stopGracePeriodSwarm: null });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings).not.toHaveProperty("StopGracePeriod");
});
});

View File

@@ -111,6 +111,7 @@ const baseApp: ApplicationNested = {
updateConfigSwarm: null,
username: null,
dockerContextPath: null,
stopGracePeriodSwarm: null,
};
const baseDomain: Domain = {

View File

@@ -25,6 +25,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
@@ -176,10 +177,18 @@ const addSwarmSettings = z.object({
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
const hasStopGracePeriodSwarm = (
value: unknown,
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
typeof value === "object" &&
value !== null &&
"stopGracePeriodSwarm" in value;
interface Props {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
@@ -224,12 +233,22 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
modeSwarm: null,
labelsSwarm: null,
networkSwarm: null,
stopGracePeriodSwarm: null,
},
resolver: zodResolver(addSwarmSettings),
});
useEffect(() => {
if (data) {
const stopGracePeriodValue = hasStopGracePeriodSwarm(data)
? data.stopGracePeriodSwarm
: null;
const normalizedStopGracePeriod =
stopGracePeriodValue === null || stopGracePeriodValue === undefined
? null
: typeof stopGracePeriodValue === "bigint"
? stopGracePeriodValue
: BigInt(stopGracePeriodValue);
form.reset({
healthCheckSwarm: data.healthCheckSwarm
? JSON.stringify(data.healthCheckSwarm, null, 2)
@@ -255,6 +274,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
networkSwarm: data.networkSwarm
? JSON.stringify(data.networkSwarm, null, 2)
: null,
stopGracePeriodSwarm: normalizedStopGracePeriod,
});
}
}, [form, form.reset, data]);
@@ -275,6 +295,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
modeSwarm: data.modeSwarm,
labelsSwarm: data.labelsSwarm,
networkSwarm: data.networkSwarm,
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
})
.then(async () => {
toast.success("Swarm settings updated");
@@ -352,9 +373,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json"
placeholder={`{
"Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"],
"Interval" : 10000,
"Timeout" : 10000,
"StartPeriod" : 10000,
"Interval" : 10000000000,
"Timeout" : 10000000000,
"StartPeriod" : 10000000000,
"Retries" : 10
}`}
className="h-[12rem] font-mono"
@@ -407,9 +428,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json"
placeholder={`{
"Condition" : "on-failure",
"Delay" : 10000,
"Delay" : 10000000000,
"MaxAttempts" : 10,
"Window" : 10000
"Window" : 10000000000
} `}
className="h-[12rem] font-mono"
{...field}
@@ -529,9 +550,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json"
placeholder={`{
"Parallelism" : 1,
"Delay" : 10000,
"Delay" : 10000000000,
"FailureAction" : "continue",
"Monitor" : 10000,
"Monitor" : 10000000000,
"MaxFailureRatio" : 10,
"Order" : "start-first"
}`}
@@ -587,9 +608,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json"
placeholder={`{
"Parallelism" : 1,
"Delay" : 10000,
"Delay" : 10000000000,
"FailureAction" : "continue",
"Monitor" : 10000,
"Monitor" : 10000000000,
"MaxFailureRatio" : 10,
"Order" : "start-first"
}`}
@@ -774,7 +795,57 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="stopGracePeriodSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormLabel>Stop Grace Period (nanoseconds)</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
Duration in nanoseconds
<HelpCircle className="size-4 text-muted-foreground" />
</FormDescription>
</TooltipTrigger>
<TooltipContent
className="w-full z-[999]"
align="start"
side="bottom"
>
<code>
<pre>
{`Enter duration in nanoseconds:
• 30000000000 - 30 seconds
• 120000000000 - 2 minutes
• 3600000000000 - 1 hour
• 0 - no grace period`}
</pre>
</code>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<FormControl>
<Input
type="number"
placeholder="30000000000"
className="font-mono"
{...field}
value={field?.value?.toString() || ""}
onChange={(e) =>
field.onChange(
e.target.value ? BigInt(e.target.value) : null,
)
}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
</FormItem>
)}
/>
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
<Button
isLoading={isLoading}

View File

@@ -1,8 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import jsyaml from "js-yaml";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { parse, stringify, YAMLParseError } from "yaml";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
@@ -38,11 +38,11 @@ interface Props {
export const validateAndFormatYAML = (yamlText: string) => {
try {
const obj = jsyaml.load(yamlText);
const formattedYaml = jsyaml.dump(obj, { indent: 4 });
const obj = parse(yamlText);
const formattedYaml = stringify(obj, { indent: 4 });
return { valid: true, formattedYaml, error: null };
} catch (error) {
if (error instanceof jsyaml.YAMLException) {
if (error instanceof YAMLParseError) {
return {
valid: false,
formattedYaml: yamlText,
@@ -89,7 +89,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: error || "Invalid YAML",
message: (error as string) || "Invalid YAML",
});
return;
}

View File

@@ -59,7 +59,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
const router = useRouter();
const { mutateAsync, isLoading } =
api.application.saveGitProdiver.useMutation();
api.application.saveGitProvider.useMutation();
const form = useForm<GitProvider>({
defaultValues: {

View File

@@ -7,7 +7,7 @@ import {
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { type Control, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
@@ -57,6 +57,7 @@ export const commonCronExpressions = [
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
{ label: "Every 15 minutes", value: "*/15 * * * *" },
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
{ label: "Custom", value: "custom" },
];
const formSchema = z
@@ -115,10 +116,91 @@ interface Props {
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
}
export const ScheduleFormField = ({
name,
formControl,
}: {
name: string;
formControl: Control<any>;
}) => {
const [selectedOption, setSelectedOption] = useState("");
return (
<FormField
control={formControl}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>Cron expression format: minute hour day month weekday</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
value={selectedOption}
onValueChange={(value) => {
setSelectedOption(value);
field.onChange(value === "custom" ? "" : value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label}
{expr.value !== "custom" && ` (${expr.value})`}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
onChange={(e) => {
const value = e.target.value;
const commonExpression = commonCronExpressions.find(
(expression) => expression.value === value,
);
if (commonExpression) {
setSelectedOption(commonExpression.value);
} else {
setSelectedOption("custom");
}
field.onChange(e);
}}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
);
};
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache");
const utils = api.useUtils();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -377,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
)}
/>
<FormField
control={form.control}
<ScheduleFormField
name="cronExpression"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
formControl={form.control}
/>
{(scheduleTypeForm === "application" ||

View File

@@ -1,11 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import {
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -47,7 +41,7 @@ import {
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import type { CacheType } from "../domains/handle-domain";
import { commonCronExpressions } from "../schedules/handle-schedules";
import { ScheduleFormField } from "../schedules/handle-schedules";
const formSchema = z
.object({
@@ -306,64 +300,9 @@ export const HandleVolumeBackups = ({
</FormItem>
)}
/>
<FormField
control={form.control}
<ScheduleFormField
name="cronExpression"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
formControl={form.control}
/>
<FormField

View File

@@ -195,6 +195,7 @@ export const ComposeActions = ({ composeId }: Props) => {
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
appType={data?.composeType || "docker-compose"}
>
<Button
variant="outline"

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -35,6 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
);
const { mutateAsync, isLoading } = api.compose.update.useMutation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const form = useForm<AddComposeFile>({
defaultValues: {
@@ -53,6 +54,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
}
}, [form, form.reset, data]);
useEffect(() => {
if (data?.composeFile !== undefined) {
setHasUnsavedChanges(composeFile !== data.composeFile);
}
}, [composeFile, data?.composeFile]);
const onSubmit = async (data: AddComposeFile) => {
const { valid, error } = validateAndFormatYAML(data.composeFile);
if (!valid) {
@@ -71,6 +78,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
})
.then(async () => {
toast.success("Compose config Updated");
setHasUnsavedChanges(false);
refetch();
await utils.compose.getConvertedCompose.invalidate({
composeId,
@@ -99,6 +107,19 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return (
<>
<div className="w-full flex flex-col gap-4 ">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">Compose File</h3>
<p className="text-sm text-muted-foreground">
Configure your Docker Compose file for this service.
{hasUnsavedChanges && (
<span className="text-yellow-500 ml-2">
(You have unsaved changes)
</span>
)}
</p>
</div>
</div>
<Form {...form}>
<form
id="hook-form-save-compose-file"

View File

@@ -37,8 +37,6 @@ interface Props {
serverId?: string;
}
badgeStateColor;
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
const [option, setOption] = useState<"swarm" | "native">("native");
const [containerId, setContainerId] = useState<string | undefined>();

View File

@@ -3,7 +3,6 @@ import {
CheckIcon,
ChevronsUpDown,
DatabaseZap,
Info,
PenBoxIcon,
PlusIcon,
RefreshCw,
@@ -62,7 +61,7 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { commonCronExpressions } from "../../application/schedules/handle-schedules";
import { ScheduleFormField } from "../../application/schedules/handle-schedules";
type CacheType = "cache" | "fetch";
@@ -579,66 +578,9 @@ export const HandleBackup = ({
);
}}
/>
<FormField
control={form.control}
name="schedule"
render={({ field }) => {
return (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<ScheduleFormField name="schedule" formControl={form.control} />
<FormField
control={form.control}
name="prefix"

View File

@@ -8,7 +8,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface Props {
id: string;
containerId: string;
containerId?: string;
serverId?: string;
}
@@ -36,7 +36,6 @@ export const DockerTerminal: React.FC<Props> = ({
},
});
const addonFit = new FitAddon();
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
@@ -57,7 +56,7 @@ export const DockerTerminal: React.FC<Props> = ({
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 mt-4">
<span>
Select way to connect to <b>{containerId}</b>
</span>

View File

@@ -171,7 +171,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
<Input
placeholder="Search Template"
onChange={(e) => setQuery(e.target.value)}
className="w-full sm:w-[200px]"
className="w-full"
value={query}
/>
<Input
@@ -248,7 +248,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
onClick={() =>
setViewMode(viewMode === "detailed" ? "icon" : "detailed")
}
className="h-9 w-9"
className="h-9 w-9 flex-shrink-0"
>
{viewMode === "detailed" ? (
<LayoutGrid className="size-4" />

View File

@@ -63,13 +63,20 @@ export const AdvancedEnvironmentSelector = ({
const [name, setName] = useState("");
const [description, setDescription] = useState("");
// API mutations
const { data: environment } = api.environment.one.useQuery(
{ environmentId: currentEnvironmentId || "" },
{
enabled: !!currentEnvironmentId,
},
);
// Get current user's permissions
const { data: currentUser } = api.user.get.useQuery();
// Check if user can create environments
const canCreateEnvironments =
currentUser?.role === "owner" ||
currentUser?.role === "admin" ||
currentUser?.canCreateEnvironments === true;
// Check if user can delete environments
const canDeleteEnvironments =
currentUser?.role === "owner" ||
currentUser?.role === "admin" ||
currentUser?.canDeleteEnvironments === true;
const haveServices =
selectedEnvironment &&
@@ -267,17 +274,19 @@ export const AdvancedEnvironmentSelector = ({
<PencilIcon className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
onClick={(e) => {
e.stopPropagation();
openDeleteDialog(environment);
}}
>
<TrashIcon className="h-3 w-3" />
</Button>
{canDeleteEnvironments && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
onClick={(e) => {
e.stopPropagation();
openDeleteDialog(environment);
}}
>
<TrashIcon className="h-3 w-3" />
</Button>
)}
</div>
)}
</div>
@@ -285,13 +294,15 @@ export const AdvancedEnvironmentSelector = ({
})}
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"
onClick={() => setIsCreateDialogOpen(true)}
>
<PlusIcon className="h-4 w-4 mr-2" />
Create Environment
</DropdownMenuItem>
{canCreateEnvironments && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => setIsCreateDialogOpen(true)}
>
<PlusIcon className="h-4 w-4 mr-2" />
Create Environment
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -88,7 +88,7 @@ export const StepThree = ({ templateInfo }: StepProps) => {
<div>
<h3 className="text-sm font-semibold">Configuration Files</h3>
<ul className="list-disc pl-5">
{templateInfo?.details?.configFiles.map((file, index) => (
{templateInfo?.details?.configFiles?.map((file, index) => (
<li key={index}>
<strong className="text-sm font-semibold">
{file.filePath}

View File

@@ -1,5 +1,5 @@
import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { Bot, PlusCircle, Trash2 } from "lucide-react";
import { useEffect } from "react";
import ReactMarkdown from "react-markdown";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
@@ -27,7 +27,6 @@ export interface StepProps {
export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
const suggestions = templateInfo.suggestions || [];
const selectedVariant = templateInfo.details;
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
const { mutateAsync, isLoading, error, isError } =
api.ai.suggest.useMutation();
@@ -44,7 +43,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
.then((data) => {
setTemplateInfo({
...templateInfo,
suggestions: data,
suggestions: data || [],
});
})
.catch((error) => {
@@ -54,10 +53,6 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
});
}, [templateInfo.userInput]);
const toggleShowValue = (name: string) => {
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
};
const handleEnvVariableChange = (
index: number,
field: "name" | "value",
@@ -308,11 +303,9 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
placeholder="Variable Name"
className="flex-1"
/>
<div className="flex-1 relative">
<div className="relative">
<Input
type={
showValues[env.name] ? "text" : "password"
}
type={"password"}
value={env.value}
onChange={(e) =>
handleEnvVariableChange(
@@ -323,19 +316,6 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
}
placeholder="Variable Value"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 transform -translate-y-1/2"
onClick={() => toggleShowValue(env.name)}
>
{showValues[env.name] ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<Button
type="button"
@@ -437,13 +417,14 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<AccordionContent>
<ScrollArea className="w-full rounded-md border">
<div className="p-4 space-y-4">
{selectedVariant?.configFiles?.length > 0 ? (
{selectedVariant?.configFiles?.length &&
selectedVariant?.configFiles?.length > 0 ? (
<>
<div className="text-sm text-muted-foreground mb-4">
This template requires the following
configuration files to be mounted:
</div>
{selectedVariant.configFiles.map(
{selectedVariant?.configFiles?.map(
(config, index) => (
<div
key={index}

View File

@@ -47,7 +47,7 @@ interface Details {
envVariables: EnvVariable[];
shortDescription: string;
domains: Domain[];
configFiles: Mount[];
configFiles?: Mount[];
}
interface Mount {

View File

@@ -80,6 +80,29 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({
onSuccess: async (newProject) => {
await utils.project.all.invalidate();
// If duplicating to same project+environment, invalidate the environment query
// to refresh the services list
if (duplicateType === "existing-environment") {
await utils.environment.one.invalidate({
environmentId: selectedTargetEnvironment,
});
await utils.environment.byProjectId.invalidate({
projectId: selectedTargetProject,
});
// If duplicating to the same environment we're currently viewing,
// also invalidate the current environment to refresh the services list
if (selectedTargetEnvironment === environmentId) {
await utils.environment.one.invalidate({ environmentId });
// Also invalidate the project query to refresh the project data
const projectId = router.query.projectId as string;
if (projectId) {
await utils.project.one.invalidate({ projectId });
}
}
}
toast.success(
duplicateType === "new-project"
? "Project duplicated successfully"

View File

@@ -96,8 +96,30 @@ export const ShowProjects = () => {
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break;
case "services": {
const aTotalServices = a.environments.length;
const bTotalServices = b.environments.length;
const aTotalServices = a.environments.reduce((total, env) => {
return (
total +
(env.applications?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
(env.postgres?.length || 0) +
(env.redis?.length || 0) +
(env.compose?.length || 0)
);
}, 0);
const bTotalServices = b.environments.reduce((total, env) => {
return (
total +
(env.applications?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
(env.postgres?.length || 0) +
(env.redis?.length || 0) +
(env.compose?.length || 0)
);
}, 0);
comparison = aTotalServices - bTotalServices;
break;
}
@@ -291,45 +313,48 @@ export const ShowProjects = () => {
)}
</DropdownMenuGroup>
)}
{/*
{project.compose.length > 0 && (
{project.environments.some(
(env) => env.compose.length > 0,
) && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Compose
</DropdownMenuLabel>
{project.compose.map((comp) => (
<div key={comp.composeId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{comp.name}
<StatusTooltip
status={comp.composeStatus}
/>
</DropdownMenuLabel>
{project.environments.map((env) =>
env.compose.map((comp) => (
<div key={comp.composeId}>
<DropdownMenuSeparator />
{comp.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{comp.name}
<StatusTooltip
status={comp.composeStatus}
/>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{comp.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
))}
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
)),
)}
</DropdownMenuGroup>
)} */}
)}
</DropdownMenuContent>
</DropdownMenu>
) : null}

View File

@@ -1,7 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -27,18 +26,12 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
username: z.string().min(1, {
message: "Username is required",
}),
password: z.string().min(1, {
message: "App Password is required",
}),
name: z.string().min(1, { message: "Name is required" }),
username: z.string().min(1, { message: "Username is required" }),
email: z.string().email().optional(),
apiToken: z.string().min(1, { message: "API Token is required" }),
workspaceName: z.string().optional(),
});
@@ -47,14 +40,12 @@ type Schema = z.infer<typeof Schema>;
export const AddBitbucketProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const _url = useUrl();
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
const { data: auth } = api.user.get.useQuery();
const _router = useRouter();
const form = useForm<Schema>({
defaultValues: {
username: "",
password: "",
apiToken: "",
workspaceName: "",
},
resolver: zodResolver(Schema),
@@ -63,7 +54,8 @@ export const AddBitbucketProvider = () => {
useEffect(() => {
form.reset({
username: "",
password: "",
email: "",
apiToken: "",
workspaceName: "",
});
}, [form, isOpen]);
@@ -71,10 +63,11 @@ export const AddBitbucketProvider = () => {
const onSubmit = async (data: Schema) => {
await mutateAsync({
bitbucketUsername: data.username,
appPassword: data.password,
apiToken: data.apiToken,
bitbucketWorkspaceName: data.workspaceName || "",
authId: auth?.id || "",
name: data.name || "",
bitbucketEmail: data.email || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
@@ -113,37 +106,46 @@ export const AddBitbucketProvider = () => {
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<AlertBlock type="warning">
Bitbucket App Passwords are deprecated for new providers. Use
an API Token instead. Existing providers with App Passwords
will continue to work until 9th June 2026.
</AlertBlock>
<div className="mt-1 text-sm">
Manage tokens in
<Link
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
className="inline-flex items-center gap-1 ml-1"
>
<span>Bitbucket settings</span>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</div>
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
<li className="text-muted-foreground text-sm">
Click on Create API token with scopes
</li>
<li className="text-muted-foreground text-sm">
Select the expiration date (Max 1 year)
</li>
<li className="text-muted-foreground text-sm">
Select Bitbucket product.
</li>
</ul>
<p className="text-muted-foreground text-sm">
To integrate your Bitbucket account, you need to create a new
App Password in your Bitbucket settings. Follow these steps:
Select the following scopes:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Create new App Password{" "}
<Link
href="https://bitbucket.org/account/settings/app-passwords/new"
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>
When creating the App Password, ensure you grant the
following permissions:
<ul className="list-disc list-inside ml-4">
<li>Account: Read</li>
<li>Workspace membership: Read</li>
<li>Projects: Read</li>
<li>Repositories: Read</li>
<li>Pull requests: Read</li>
<li>Webhooks: Read and write</li>
</ul>
</li>
<li>
After creating, you'll receive an App Password. Copy it and
paste it below along with your Bitbucket username.
</li>
</ol>
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
<li>read:repository:bitbucket</li>
<li>read:pullrequest:bitbucket</li>
<li>read:webhook:bitbucket</li>
<li>read:workspace:bitbucket</li>
<li>write:webhook:bitbucket</li>
</ul>
<FormField
control={form.control}
name="name"
@@ -152,7 +154,7 @@ export const AddBitbucketProvider = () => {
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
placeholder="Your Bitbucket Provider, eg: my-personal-account"
{...field}
/>
</FormControl>
@@ -179,14 +181,27 @@ export const AddBitbucketProvider = () => {
<FormField
control={form.control}
name="password"
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>App Password</FormLabel>
<FormLabel>Bitbucket Email</FormLabel>
<FormControl>
<Input placeholder="Your Bitbucket email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiToken"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
placeholder="Paste your Bitbucket API token"
{...field}
/>
</FormControl>
@@ -200,7 +215,7 @@ export const AddBitbucketProvider = () => {
name="workspaceName"
render={({ field }) => (
<FormItem>
<FormLabel>Workspace Name (Optional)</FormLabel>
<FormLabel>Workspace Name (optional)</FormLabel>
<FormControl>
<Input
placeholder="For organization accounts"

View File

@@ -33,7 +33,10 @@ const Schema = z.object({
username: z.string().min(1, {
message: "Username is required",
}),
email: z.string().email().optional(),
workspaceName: z.string().optional(),
apiToken: z.string().optional(),
appPassword: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
@@ -60,19 +63,28 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
const form = useForm<Schema>({
defaultValues: {
username: "",
email: "",
workspaceName: "",
apiToken: "",
appPassword: "",
},
resolver: zodResolver(Schema),
});
const username = form.watch("username");
const email = form.watch("email");
const workspaceName = form.watch("workspaceName");
const apiToken = form.watch("apiToken");
const appPassword = form.watch("appPassword");
useEffect(() => {
form.reset({
username: bitbucket?.bitbucketUsername || "",
email: bitbucket?.bitbucketEmail || "",
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
name: bitbucket?.gitProvider.name || "",
apiToken: bitbucket?.apiToken || "",
appPassword: bitbucket?.appPassword || "",
});
}, [form, isOpen, bitbucket]);
@@ -81,8 +93,11 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
bitbucketId,
gitProviderId: bitbucket?.gitProviderId || "",
bitbucketUsername: data.username,
bitbucketEmail: data.email || "",
bitbucketWorkspaceName: data.workspaceName || "",
name: data.name || "",
apiToken: data.apiToken || "",
appPassword: data.appPassword || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
@@ -121,6 +136,12 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
Update your Bitbucket authentication. Use API Token for
enhanced security (recommended) or App Password for legacy
support.
</p>
<FormField
control={form.control}
name="name"
@@ -154,6 +175,24 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email (Required for API Tokens)</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Your Bitbucket email address"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="workspaceName"
@@ -171,6 +210,49 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
)}
/>
<div className="flex flex-col gap-2 border-t pt-4">
<h3 className="text-sm font-medium mb-2">
Authentication (Update to use API Token)
</h3>
<FormField
control={form.control}
name="apiToken"
render={({ field }) => (
<FormItem>
<FormLabel>API Token (Recommended)</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Bitbucket API Token"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appPassword"
render={({ field }) => (
<FormItem>
<FormLabel>
App Password (Legacy - will be deprecated June 2026)
</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Bitbucket App Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-between gap-4 mt-4">
<Button
type="button"
@@ -180,7 +262,10 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
await testConnection({
bitbucketId,
bitbucketUsername: username,
bitbucketEmail: email,
workspaceName: workspaceName,
apiToken: apiToken,
appPassword: appPassword,
})
.then(async (message) => {
toast.info(`Message: ${message}`);

View File

@@ -30,6 +30,9 @@ const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
appName: z.string().min(1, {
message: "App Name is required",
}),
});
type Schema = z.infer<typeof Schema>;
@@ -55,6 +58,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
const form = useForm<Schema>({
defaultValues: {
name: "",
appName: "",
},
resolver: zodResolver(Schema),
});
@@ -62,6 +66,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
useEffect(() => {
form.reset({
name: github?.gitProvider.name || "",
appName: github?.githubAppName || "",
});
}, [form, isOpen]);
@@ -70,6 +75,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
githubId,
name: data.name || "",
gitProviderId: github?.gitProviderId || "",
githubAppName: data.appName || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
@@ -124,6 +130,22 @@ export const EditGithubProvider = ({ githubId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>App Name</FormLabel>
<FormControl>
<Input
placeholder="pp Name eg(my-personal)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex w-full justify-between gap-4 mt-4">
<Button

View File

@@ -157,7 +157,13 @@ export const ShowGitProviders = () => {
</div>
</div>
<div className="flex flex-row gap-1">
<div className="flex flex-row gap-1 items-center">
{isBitbucket &&
gitProvider.bitbucket?.appPassword &&
!gitProvider.bitbucket?.apiToken ? (
<Badge variant="yellow">Deprecated</Badge>
) : null}
{!haveGithubRequirements && isGithub && (
<div className="flex flex-row gap-1 items-center">
<Badge

View File

@@ -12,6 +12,8 @@ import { toast } from "sonner";
import { z } from "zod";
import {
DiscordIcon,
GotifyIcon,
NtfyIcon,
SlackIcon,
TelegramIcon,
} from "@/components/icons/notification-icons";
@@ -130,11 +132,11 @@ export const notificationsMap = {
label: "Email",
},
gotify: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
icon: <GotifyIcon />,
label: "Gotify",
},
ntfy: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
icon: <NtfyIcon />,
label: "ntfy",
},
};

View File

@@ -2,6 +2,8 @@ import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
import { toast } from "sonner";
import {
DiscordIcon,
GotifyIcon,
NtfyIcon,
SlackIcon,
TelegramIcon,
} from "@/components/icons/notification-icons";
@@ -85,12 +87,12 @@ export const ShowNotifications = () => {
)}
{notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
<GotifyIcon className="size-6" />
</div>
)}
{notification.notificationType === "ntfy" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
<NtfyIcon className="size-6" />
</div>
)}

View File

@@ -33,7 +33,10 @@ import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa";
const profileSchema = z.object({
email: z.string(),
email: z
.string()
.email("Please enter a valid email address")
.min(1, "Email is required"),
password: z.string().nullable(),
currentPassword: z.string().nullable(),
image: z.string().optional(),
@@ -254,8 +257,16 @@ export const ProfileForm = () => {
onValueChange={(e) => {
field.onChange(e);
}}
defaultValue={field.value}
value={field.value}
defaultValue={
field.value?.startsWith("data:")
? "upload"
: field.value
}
value={
field.value?.startsWith("data:")
? "upload"
: field.value
}
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
>
<FormItem key="no-avatar">
@@ -276,6 +287,71 @@ export const ProfileForm = () => {
</Avatar>
</FormLabel>
</FormItem>
<FormItem key="custom-upload">
<FormLabel className="[&:has([data-state=checked])>.upload-avatar]:border-primary [&:has([data-state=checked])>.upload-avatar]:border-1 [&:has([data-state=checked])>.upload-avatar]:p-px cursor-pointer">
<FormControl>
<RadioGroupItem
value="upload"
className="sr-only"
/>
</FormControl>
<div
className="upload-avatar h-12 w-12 rounded-full border border-dashed border-muted-foreground hover:border-primary transition-colors flex items-center justify-center bg-muted/50 hover:bg-muted overflow-hidden"
onClick={() =>
document
.getElementById("avatar-upload")
?.click()
}
>
{field.value?.startsWith("data:") ? (
<img
src={field.value}
alt="Custom avatar"
className="h-full w-full object-cover rounded-full"
/>
) : (
<svg
className="h-5 w-5 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
)}
</div>
<input
id="avatar-upload"
type="file"
accept="image/*"
className="hidden"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
// max file size 2mb
if (file.size > 2 * 1024 * 1024) {
toast.error(
"Image size must be less than 2MB",
);
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target
?.result as string;
field.onChange(result);
};
reader.readAsDataURL(file);
}
}}
/>
</FormLabel>
</FormItem>
{availableAvatars.map((image) => (
<FormItem key={image}>
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">

View File

@@ -1,6 +1,6 @@
import type { findEnvironmentById } from "@dokploy/server/index";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -161,11 +161,13 @@ const addPermissions = z.object({
canCreateServices: z.boolean().optional().default(false),
canDeleteProjects: z.boolean().optional().default(false),
canDeleteServices: z.boolean().optional().default(false),
canDeleteEnvironments: z.boolean().optional().default(false),
canAccessToTraefikFiles: z.boolean().optional().default(false),
canAccessToDocker: z.boolean().optional().default(false),
canAccessToAPI: z.boolean().optional().default(false),
canAccessToSSHKeys: z.boolean().optional().default(false),
canAccessToGitProviders: z.boolean().optional().default(false),
canCreateEnvironments: z.boolean().optional().default(false),
});
type AddPermissions = z.infer<typeof addPermissions>;
@@ -175,6 +177,7 @@ interface Props {
}
export const AddUserPermissions = ({ userId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data: projects } = api.project.all.useQuery();
const { data, refetch } = api.user.one.useQuery(
@@ -192,13 +195,25 @@ export const AddUserPermissions = ({ userId }: Props) => {
const form = useForm<AddPermissions>({
defaultValues: {
accessedProjects: [],
accessedEnvironments: [],
accessedServices: [],
canDeleteEnvironments: false,
canCreateProjects: false,
canCreateServices: false,
canDeleteProjects: false,
canDeleteServices: false,
canAccessToTraefikFiles: false,
canAccessToDocker: false,
canAccessToAPI: false,
canAccessToSSHKeys: false,
canAccessToGitProviders: false,
canCreateEnvironments: false,
},
resolver: zodResolver(addPermissions),
});
useEffect(() => {
if (data) {
if (data && isOpen) {
form.reset({
accessedProjects: data.accessedProjects || [],
accessedEnvironments: data.accessedEnvironments || [],
@@ -207,14 +222,16 @@ export const AddUserPermissions = ({ userId }: Props) => {
canCreateServices: data.canCreateServices,
canDeleteProjects: data.canDeleteProjects,
canDeleteServices: data.canDeleteServices,
canDeleteEnvironments: data.canDeleteEnvironments || false,
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
canAccessToGitProviders: data.canAccessToGitProviders,
canCreateEnvironments: data.canCreateEnvironments,
});
}
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
}, [form, form.reset, data, isOpen]);
const onSubmit = async (data: AddPermissions) => {
await mutateAsync({
@@ -223,6 +240,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canCreateProjects: data.canCreateProjects,
canDeleteServices: data.canDeleteServices,
canDeleteProjects: data.canDeleteProjects,
canDeleteEnvironments: data.canDeleteEnvironments,
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
accessedProjects: data.accessedProjects || [],
accessedEnvironments: data.accessedEnvironments || [],
@@ -231,17 +249,19 @@ export const AddUserPermissions = ({ userId }: Props) => {
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
canAccessToGitProviders: data.canAccessToGitProviders,
canCreateEnvironments: data.canCreateEnvironments,
})
.then(async () => {
toast.success("Permissions updated");
refetch();
setIsOpen(false);
})
.catch(() => {
toast.error("Error updating the permissions");
});
};
return (
<Dialog>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
@@ -343,6 +363,46 @@ export const AddUserPermissions = ({ userId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="canCreateEnvironments"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Create Environments</FormLabel>
<FormDescription>
Allow the user to create environments
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="canDeleteEnvironments"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Delete Environments</FormLabel>
<FormDescription>
Allow the user to delete environments
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="canAccessToTraefikFiles"

View File

@@ -5,6 +5,7 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -76,6 +77,9 @@ export const WebDomain = () => {
resolver: zodResolver(addServerDomain),
});
const https = form.watch("https");
const domain = form.watch("domain") || "";
const host = data?.user?.host || "";
const hasChanged = domain !== host;
useEffect(() => {
if (data) {
form.reset({
@@ -119,6 +123,19 @@ export const WebDomain = () => {
</div>
</CardHeader>
<CardContent className="space-y-2 py-6 border-t">
{/* Warning for GitHub webhook URL changes */}
{hasChanged && (
<AlertBlock type="warning">
<div className="space-y-2">
<p className="font-medium"> Important: URL Change Impact</p>
<p>
If you change the Dokploy Server URL make sure to update
your Github Apps to keep the auto-deploy working and preview
deployments working.
</p>
</div>
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -13,7 +13,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
@@ -40,18 +39,26 @@ interface Props {
appName: string;
children?: React.ReactNode;
serverId?: string;
appType?: "stack" | "docker-compose";
}
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
export const DockerTerminalModal = ({
children,
appName,
serverId,
appType,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
appType,
serverId,
},
{
enabled: !!appName,
},
);
const [containerId, setContainerId] = useState<string | undefined>();
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
@@ -83,7 +90,7 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="max-h-[85vh] sm:max-w-7xl"
className="max-h-[85vh] sm:max-w-7xl"
onEscapeKeyDown={(event) => event.preventDefault()}
>
<DialogHeader>
@@ -92,7 +99,6 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
Easy way to access to docker container
</DialogDescription>
</DialogHeader>
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{isLoading ? (

View File

@@ -88,3 +88,121 @@ export const DiscordIcon = ({ className }: Props) => {
</svg>
);
};
export const GotifyIcon = ({ className }: Props) => {
return (
<svg
viewBox="0 0 500 500"
className={cn("size-8", className)}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<style>
{`
.gotify-st0{fill:#DDCBA2;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st1{fill:#71CAEE;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st2{fill:#FFFFFF;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st3{fill:#888E93;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st4{fill:#F0F0F0;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st5{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st8{fill:#FFFFFF;}
`}
</style>
<linearGradient
id="gotify-gradient"
x1="265"
y1="280"
x2="275"
y2="302"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#71CAEE" />
<stop offset="0.04" stopColor="#83CAE2" />
<stop offset="0.12" stopColor="#9FCACE" />
<stop offset="0.21" stopColor="#B6CBBE" />
<stop offset="0.31" stopColor="#C7CBB1" />
<stop offset="0.44" stopColor="#D4CBA8" />
<stop offset="0.61" stopColor="#DBCBA3" />
<stop offset="1" stopColor="#DDCBA2" />
</linearGradient>
</defs>
<g transform="matrix(2.33,0,0,2.33,-432,-323)">
<g transform="translate(-25,26)">
<path
className="gotify-st1"
d="m258.9,119.7c-3,-0.9-6,-1.8-9,-2.7-4.6,-1.4-9.2,-2.8-14,-2.5-2.8,0.2-6.1,1.3-6.9,4-0.6,2-1.6,7.3-1.3,7.9 1.5,3.4 13.9,6.7 18.3,6.7"
/>
<path d="m392.6,177.9c-1.4,1.4-2.2,3.5-2.5,5.5-0.2,1.4-0.1,3 0.5,4.3 0.6,1.3 1.8,2.3 3.1,3 1.3,0.6 2.8,0.9 4.3,0.9 1.1,0 2.3,-0.1 3.1,-0.9 0.6,-0.7 0.8,-1.6 0.9,-2.5 0.2,-2.3-0.1,-4.7-0.9,-6.9-0.4,-1.1-0.9,-2.3-1.8,-3.1-1.7,-1.8-4.5,-2.2-6.4,-0.5-0.1,0-0.2,0.1-0.3,0.2z" />
<path
className="gotify-st2"
d="m358.5,164.2c-1,-1 0,-2.7 1,-3.7 5.8,-5.2 15.1,-4.6 21.8,-0.6 10.9,6.6 15.6,19.9 17.2,32.5 0.6,5.2 0.9,10.6-0.5,15.7-1.4,5.1-4.6,9.9-9.3,12.1-1.1,0.5-2.3,0.9-3.4,0.5-1.1,-0.4-1.9,-1.8-1.2,-2.8-9.4,-13.6-19,-26.8-20.9,-43.2-0.5,-4.1-1.8,-7.4-4.7,-10.5z"
/>
<path
className="gotify-st1"
d="m220.1,133c34.6,-18 79.3,-19.6 112.2,-8.7 23.7,7.9 41.3,26.7 49.5,50 7.1,20.6 7.1,43.6 3,65.7-7.5,40.2-26.2,77.9-49,112.6-12.6,19-24.6,36-44.2,48.5-38.7,24.6-88.9,22.1-129.3,11.5-19.5,-5.1-38.4,-17.3-44.3,-37.3-3.8,-12.8-2.1,-27.6 4.6,-40 13.5,-24.8 46.2,-38.4 50.8,-67.9 1.4,-8.7-0.3,-17.3-1.6,-25.7-3.8,-23.4-5.4,-45.8 6.7,-68.7 9.5,-17.7 24.3,-31 41.7,-40z"
/>
<path
className="gotify-st2"
d="m264.5,174.9c-0.5,0.5-0.9,1-1.3,1.6-9,11.6-12,27.9-9.3,42.1 1.7,9 5.9,17.9 13.2,23.4 19.3,14.6 51.5,13.5 68.4,-1.5 24.4,-21.7 13,-67.6-14,-78.8-17.6,-7.2-43.7,-1.6-57,13.2z"
/>
<path
className="gotify-st2"
d="m382.1,237.1c1.4,-0.1 2.9,-0.1 4.3,0.1 0.3,0 0.7,0.1 1,0.4 0.2,0.3 0.4,0.7 0.5,1.1 1,3.9 0.5,8.2 0.1,12.4-0.1,0.9-0.2,1.8-0.6,2.6-1,2.1-3.1,2.7-4.7,2.7-0.1,0-0.2,0-0.3,-0.1-0.3,-0.2-0.3,-0.7-0.2,-1.2 0.3,-5.9-0.1,-11.9-0.1,-18z"
/>
<path
className="gotify-st2"
d="m378.7,236.8c-1.4,0.4-2.5,2-2.8,4.4-0.5,4.4-0.7,8.9-0.5,13.4 0,0.9 0.1,1.9 0.5,2.4 0.2,0.3 0.5,0.4 0.8,0.4 1.6,0.3 4.1,-0.6 5.6,-1 0,0 0,-5.2-0.1,-8-0.1,-2.8-0.1,-6.1-0.2,-8.9 0,-0.6 0,-1.5 0,-2.2 0.1,-0.7-2.6,-0.7-3.3,-0.5z"
/>
<path
className="gotify-st0"
d="m358.3,231.8c-0.3,2.2 0.1,4.7 1.7,7.4 2.6,4.4 7,6.1 11.9,5.8 8.9,-0.6 25.3,-5.4 27.5,-15.7 0.6,-3-0.3,-6.1-2.2,-8.5-6.2,-7.8-17.8,-5.7-25.6,-2-5.9,2.7-12.4,7-13.3,13z"
/>
<path
className="gotify-st3"
d="m386.4,208.6c2.2,1.4 3.7,3.8 4,7 0.3,3.6-1.4,7.5-5,8.8-2.9,1.1-6.2,0.6-9.1,-0.4-2.9,-1-5.8,-2.8-6.8,-5.7-0.7,-2-0.3,-4.3 0.7,-6.1 1.1,-1.8 2.8,-3.2 4.7,-4.1 3.9,-1.8 8.4,-1.6 11.5,0.5z"
/>
<path
className="gotify-st0"
d="m414.7,262.6c2.4,0.6 4.8,2.1 5.6,4.4 0.8,2.3 0.1,4.9-1.6,6.7-1.7,1.8-4.2,2.5-6.6,2.5-0.8,0-1.7,-0.1-2.4,-0.5-2.5,-1.1-3.5,-4-4.2,-6.6-1.8,-6.8 3.6,-7.8 9.2,-6.5z"
/>
<path
className="gotify-st4"
d="m267.1,284.7c2.3,-4.5 141.3,-36.2 144.7,-31.6 3.4,4.5 15.8,88.2 9,90.4-6.8,2.3-119.8,37.3-126.6,35-6.8,-2.3-29.4,-89.3-27.1,-93.8z"
/>
<path
className="gotify-st5"
d="m294.2,378.5c0,0 54.3,-74.6 59.9,-76.9 5.7,-2.3 67.3,41.3 67.3,41.3"
/>
<path
className="gotify-st4"
d="m267,287.7c0,0 86,38.8 91.6,36.6 5.7,-2.3 53.1,-71.2 53.1,-71.2"
/>
<path
fill="url(#gotify-gradient)"
d="m261.9,283.5c-0.1,4.2 4.3,7.3 8.4,7.6 4.1,0.3 8.2,-1.3 12.2,-2.6 1.4,-0.4 2.9,-0.8 4.2,-0.2 1.8,0.9 2.7,4.1 1.8,5.9-0.9,1.8-3.4,3.5-5.3,4.4-6.5,3-12.9,3.6-19.9,2-5.3,-1.2-11.3,-4.3-13,-13.5"
/>
<path d="m318.4,198.4c-2,-0.3-4.1,0.1-5.9,1.3-3.2,2.1-4.7,6.2-4.7,9.9 0,1.9 0.4,3.8 1.4,5.3 1.2,1.7 3.1,2.9 5.2,3.4 3.4,0.8 8.2,0.7 10.5,-2.5 1,-1.5 1.4,-3.3 1.5,-5.1 0.5,-5.7-1.8,-11.4-8,-12.3z" />
<path
className="gotify-st8"
d="m320.4,203.3c0.9,0.3 1.7,0.8 2.1,1.7 0.4,0.8 0.4,1.7 0.3,2.5-0.1,1-0.6,2-1.5,2.7-0.7,0.5-1.7,0.7-2.6,0.5-0.9,-0.2-1.7,-0.8-2.2,-1.6-1.1,-1.6-0.9,-4.4 0.9,-5.5 0.9,-0.4 2,-0.6 3,-0.3z"
/>
</g>
</g>
</svg>
);
};
export const NtfyIcon = ({ className }: Props) => {
return (
<svg
viewBox="0 0 24 24"
className={cn("size-8", className)}
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M12.597 13.693v2.156h6.205v-2.156ZM5.183 6.549v2.363l3.591 1.901 0.023 0.01 -0.023 0.009 -3.591 1.901v2.35l0.386 -0.211 5.456 -2.969V9.729ZM3.659 2.037C1.915 2.037 0.42 3.41 0.42 5.154v0.002L0.438 18.73 0 21.963l5.956 -1.583h14.806c1.744 0 3.238 -1.374 3.238 -3.118V5.154c0 -1.744 -1.493 -3.116 -3.237 -3.117h-0.001zm0 2.2h17.104c0.613 0.001 1.037 0.447 1.037 0.917v12.108c0 0.47 -0.424 0.916 -1.038 0.916H5.633l-3.026 0.915 0.031 -0.179 -0.017 -13.76c0 -0.47 0.424 -0.917 1.038 -0.917z"
/>
</svg>
);
};

View File

@@ -7,7 +7,14 @@ import {
import { cn } from "@/lib/utils";
interface Props {
status: "running" | "error" | "done" | "idle" | undefined | null;
status:
| "running"
| "error"
| "done"
| "idle"
| "cancelled"
| undefined
| null;
className?: string;
}
@@ -34,6 +41,14 @@ export const StatusTooltip = ({ status, className }: Props) => {
className={cn("size-3.5 rounded-full bg-green-500", className)}
/>
)}
{status === "cancelled" && (
<div
className={cn(
"size-3.5 rounded-full bg-muted-foreground",
className,
)}
/>
)}
{status === "running" && (
<div
className={cn("size-3.5 rounded-full bg-yellow-500", className)}
@@ -46,6 +61,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
{status === "error" && "Error"}
{status === "done" && "Done"}
{status === "running" && "Running"}
{status === "cancelled" && "Cancelled"}
</span>
</TooltipContent>
</Tooltip>

View File

@@ -0,0 +1 @@
ALTER TABLE "bitbucket" ADD COLUMN "apiToken" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "bitbucket" ADD COLUMN "bitbucketEmail" text;

View File

@@ -0,0 +1 @@
ALTER TYPE "public"."deploymentStatus" ADD VALUE 'cancelled';

View File

@@ -0,0 +1,6 @@
ALTER TABLE "application" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "stopGracePeriodSwarm" bigint;

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -778,6 +778,48 @@
"when": 1757189541734,
"tag": "0110_red_psynapse",
"breakpoints": true
},
{
"idx": 111,
"version": "7",
"when": 1758445844561,
"tag": "0111_mushy_wolfsbane",
"breakpoints": true
},
{
"idx": 112,
"version": "7",
"when": 1758483520214,
"tag": "0112_freezing_skrulls",
"breakpoints": true
},
{
"idx": 113,
"version": "7",
"when": 1758960816504,
"tag": "0113_complete_rafael_vega",
"breakpoints": true
},
{
"idx": 114,
"version": "7",
"when": 1759643172958,
"tag": "0114_dry_black_tom",
"breakpoints": true
},
{
"idx": 115,
"version": "7",
"when": 1759644540829,
"tag": "0115_serious_black_bird",
"breakpoints": true
},
{
"idx": 116,
"version": "7",
"when": 1759645163834,
"tag": "0116_amusing_firedrake",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.25.1",
"version": "v0.25.5",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -112,7 +112,7 @@
"i18next": "^23.16.8",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"js-yaml": "4.1.0",
"yaml": "2.8.1",
"lodash": "4.17.21",
"lucide-react": "^0.469.0",
"micromatch": "4.0.8",
@@ -160,7 +160,6 @@
"@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2",
"@types/js-cookie": "^3.0.6",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4",
"@types/micromatch": "4.0.9",
"@types/node": "^18.19.104",

View File

@@ -1,4 +1,9 @@
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
import {
type Bitbucket,
getBitbucketHeaders,
IS_CLOUD,
shouldDeploy,
} from "@dokploy/server";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import { db } from "@/server/db";
@@ -146,10 +151,10 @@ export default async function handler(
const commitedPaths = await extractCommitedPaths(
req.body,
application.bitbucketOwner,
application.bitbucket?.appPassword || "",
application.bitbucket,
application.bitbucketRepository || "",
);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
commitedPaths,
@@ -354,9 +359,8 @@ export const getProviderByHeader = (headers: any) => {
export const extractCommitedPaths = async (
body: any,
bitbucketUsername: string | null,
bitbucketAppPassword: string | null,
repository: string | null,
bitbucket: Bitbucket | null,
repository: string,
) => {
const changes = body.push?.changes || [];
@@ -365,18 +369,16 @@ export const extractCommitedPaths = async (
.filter(Boolean);
const commitedPaths: string[] = [];
for (const commit of commitHashes) {
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketUsername}/${repository}/diffstat/${commit}`;
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucket?.bitbucketUsername}/${repository}/diffstat/${commit}`;
try {
const response = await fetch(url, {
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketUsername}:${bitbucketAppPassword}`).toString("base64")}`,
},
headers: getBitbucketHeaders(bitbucket!),
});
const data = await response.json();
for (const value of data.values) {
commitedPaths.push(value.new?.path);
if (value?.new?.path) commitedPaths.push(value.new.path);
}
} catch (error) {
console.error(

View File

@@ -99,8 +99,7 @@ export default async function handler(
const commitedPaths = await extractCommitedPaths(
req.body,
composeResult.bitbucketOwner,
composeResult.bitbucket?.appPassword || "",
composeResult.bitbucket,
composeResult.bitbucketRepository || "",
);

View File

@@ -226,6 +226,7 @@ const Service = (
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="preview-deployments">
Preview Deployments
</TabsTrigger>
@@ -233,7 +234,6 @@ const Service = (
<TabsTrigger value="volume-backups">
Volume Backups
</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>

View File

@@ -524,7 +524,7 @@ export const applicationRouter = createTRPCRouter({
return true;
}),
saveGitProdiver: protectedProcedure
saveGitProvider: protectedProcedure
.input(apiSaveGitProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);

View File

@@ -7,7 +7,7 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { getPublicIpWithFallback } from "@/server/wss/terminal";
import { getLocalServerIp } from "@/server/wss/terminal";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const clusterRouter = createTRPCRouter({
getNodes: protectedProcedure
@@ -61,7 +61,7 @@ export const clusterRouter = createTRPCRouter({
const result = await docker.swarmInspect();
const docker_version = await docker.version();
let ip = await getPublicIpWithFallback();
let ip = await getLocalServerIp();
if (input.serverId) {
const server = await findServerById(input.serverId);
ip = server?.ipAddress;
@@ -85,7 +85,7 @@ export const clusterRouter = createTRPCRouter({
const result = await docker.swarmInspect();
const docker_version = await docker.version();
let ip = await getPublicIpWithFallback();
let ip = await getLocalServerIp();
if (input.serverId) {
const server = await findServerById(input.serverId);
ip = server?.ipAddress;

View File

@@ -39,10 +39,10 @@ import {
import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { parse } from "toml";
import { stringify } from "yaml";
import { z } from "zod";
import { slugify } from "@/lib/slug";
import { db } from "@/server/db";
@@ -364,7 +364,7 @@ export const composeRouter = createTRPCRouter({
}
const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains);
return dump(composeFile, {
return stringify(composeFile, {
lineWidth: 1000,
});
}),

View File

@@ -1,6 +1,8 @@
import {
addNewEnvironment,
checkEnvironmentAccess,
checkEnvironmentCreationPermission,
checkEnvironmentDeletionPermission,
createEnvironment,
deleteEnvironment,
duplicateEnvironment,
@@ -54,9 +56,12 @@ export const environmentRouter = createTRPCRouter({
.input(apiCreateEnvironment)
.mutation(async ({ input, ctx }) => {
try {
// Check if user has access to the project
// This would typically involve checking project ownership/membership
// For now, we'll use a basic organization check
// Check if user has permission to create environments
await checkEnvironmentCreationPermission(
ctx.user.id,
input.projectId,
ctx.session.activeOrganizationId,
);
if (input.name === "production") {
throw new TRPCError({
@@ -76,6 +81,9 @@ export const environmentRouter = createTRPCRouter({
}
return environment;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error creating the environment: ${error instanceof Error ? error.message : error}`,
@@ -187,14 +195,6 @@ export const environmentRouter = createTRPCRouter({
.input(apiRemoveEnvironment)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.role === "member") {
await checkEnvironmentAccess(
ctx.user.id,
input.environmentId,
ctx.session.activeOrganizationId,
"access",
);
}
const environment = await findEnvironmentById(input.environmentId);
if (
environment.project.organizationId !==
@@ -206,27 +206,33 @@ export const environmentRouter = createTRPCRouter({
});
}
// Check environment access for members
if (ctx.user.role === "member") {
const { accessedEnvironments } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
// Check environment deletion permission
await checkEnvironmentDeletionPermission(
ctx.user.id,
environment.projectId,
ctx.session.activeOrganizationId,
);
if (!accessedEnvironments.includes(environment.environmentId)) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to delete this environment",
});
}
// Additional check for environment access for members
if (ctx.user.role === "member") {
await checkEnvironmentAccess(
ctx.user.id,
input.environmentId,
ctx.session.activeOrganizationId,
"access",
);
}
const deletedEnvironment = await deleteEnvironment(input.environmentId);
return deletedEnvironment;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error deleting the environment: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),

View File

@@ -3,6 +3,7 @@ import {
getGithubBranches,
getGithubRepositories,
haveGithubRequirements,
updateGithub,
updateGitProvider,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
@@ -134,5 +135,9 @@ export const githubRouter = createTRPCRouter({
name: input.name,
organizationId: ctx.session.activeOrganizationId,
});
await updateGithub(input.githubId, {
...input,
});
}),
});

View File

@@ -1,4 +1,5 @@
import {
addNewEnvironment,
addNewProject,
checkProjectAccess,
createApplication,
@@ -85,6 +86,12 @@ export const projectRouter = createTRPCRouter({
project.project.projectId,
ctx.session.activeOrganizationId,
);
await addNewEnvironment(
ctx.user.id,
project?.environment?.environmentId || "",
ctx.session.activeOrganizationId,
);
}
return project;

View File

@@ -46,8 +46,8 @@ import {
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { eq, sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { parse, stringify } from "yaml";
import { z } from "zod";
import { db } from "@/server/db";
import {
@@ -657,7 +657,7 @@ export const settingsRouter = createTRPCRouter({
const config = readMainConfig();
if (!config) return false;
const parsedConfig = load(config) as {
const parsedConfig = parse(config) as {
accessLog?: {
filePath: string;
};
@@ -678,7 +678,7 @@ export const settingsRouter = createTRPCRouter({
const mainConfig = readMainConfig();
if (!mainConfig) return false;
const currentConfig = load(mainConfig) as {
const currentConfig = parse(mainConfig) as {
accessLog?: {
filePath: string;
};
@@ -701,7 +701,7 @@ export const settingsRouter = createTRPCRouter({
currentConfig.accessLog = undefined;
}
writeMainConfig(dump(currentConfig));
writeMainConfig(stringify(currentConfig));
return true;
}),

View File

@@ -192,7 +192,16 @@ export const userRouter = createTRPCRouter({
})
.where(eq(account.userId, ctx.user.id));
}
return await updateUser(ctx.user.id, input);
try {
return await updateUser(ctx.user.id, input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error ? error.message : "Failed to update user",
});
}
}),
getUserByToken: publicProcedure
.input(apiFindOneToken)

View File

@@ -8,6 +8,7 @@ import {
initializeNetwork,
initSchedules,
initVolumeBackupsCronJobs,
initCancelDeployments,
sendDokployRestartNotifications,
setupDirectories,
} from "@dokploy/server";
@@ -52,6 +53,7 @@ void app.prepare().then(async () => {
await migration();
await initCronJobs();
await initSchedules();
await initCancelDeployments();
await initVolumeBackupsCronJobs();
await sendDokployRestartNotifications();
}

View File

@@ -1,5 +1,10 @@
import type http from "node:http";
import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server";
import {
execAsync,
findServerById,
IS_CLOUD,
validateRequest,
} from "@dokploy/server";
import { publicIpv4, publicIpv6 } from "public-ip";
import { Client, type ConnectConfig } from "ssh2";
import { WebSocketServer } from "ws";
@@ -44,6 +49,21 @@ export const getPublicIpWithFallback = async () => {
return ip;
};
export const getLocalServerIp = async () => {
try {
const command = `ip addr show | grep -E "inet (192\.168\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.)" | head -n1 | awk '{print $2}' | cut -d/ -f1`;
const { stdout } = await execAsync(command);
const ip = stdout.trim();
return (
ip ||
"We were unable to obtain the local server IP, please use your private IP address"
);
} catch (error) {
console.error("Error to obtain local server IP", error);
return "We were unable to obtain the local server IP, please use your private IP address";
}
};
export const setupTerminalWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {

View File

@@ -1,3 +1,4 @@
import { exit } from "node:process";
import { execAsync } from "@dokploy/server";
import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
@@ -25,6 +26,8 @@ import {
await initializeStandaloneTraefik();
await initializeRedis();
await initializePostgres();
console.log("Dokploy setup completed");
exit(0);
} catch (e) {
console.error("Error in dokploy setup", e);
}

View File

@@ -11,7 +11,7 @@ import {
} from "./queue.js";
import { jobQueueSchema } from "./schema.js";
import { initializeJobs } from "./utils.js";
import { firstWorker, secondWorker } from "./workers.js";
import { firstWorker, secondWorker, thirdWorker } from "./workers.js";
const app = new Hono();
@@ -91,6 +91,7 @@ export const gracefulShutdown = async (signal: string) => {
logger.warn(`Received ${signal}, closing server...`);
await firstWorker.close();
await secondWorker.close();
await thirdWorker.close();
process.exit(0);
};

View File

@@ -7,22 +7,34 @@ import { runJobs } from "./utils.js";
export const firstWorker = new Worker(
"backupQueue",
async (job: Job<QueueJob>) => {
logger.info({ data: job.data }, "Running job");
logger.info({ data: job.data }, "Running job first worker");
await runJobs(job.data);
},
{
concurrency: 50,
concurrency: 100,
connection,
},
);
export const secondWorker = new Worker(
"backupQueue",
async (job: Job<QueueJob>) => {
logger.info({ data: job.data }, "Running job");
logger.info({ data: job.data }, "Running job second worker");
await runJobs(job.data);
},
{
concurrency: 50,
concurrency: 100,
connection,
},
);
export const thirdWorker = new Worker(
"backupQueue",
async (job: Job<QueueJob>) => {
logger.info({ data: job.data }, "Running job third worker");
await runJobs(job.data);
},
{
concurrency: 100,
connection,
},
);

View File

@@ -57,7 +57,7 @@
"drizzle-orm": "^0.39.3",
"drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"js-yaml": "4.1.0",
"yaml": "2.8.1",
"lodash": "4.17.21",
"micromatch": "4.0.8",
"nanoid": "3.3.11",
@@ -85,7 +85,6 @@
"@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4",
"@types/micromatch": "4.0.9",
"@types/node": "^18.19.104",

View File

@@ -108,6 +108,12 @@ export const member = pgTable("member", {
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull()
.default(false),
canDeleteEnvironments: boolean("canDeleteEnvironments")
.notNull()
.default(false),
canCreateEnvironments: boolean("canCreateEnvironments")
.notNull()
.default(false),
accessedProjects: text("accesedProjects")
.array()
.notNull()

View File

@@ -1,5 +1,6 @@
import { relations } from "drizzle-orm";
import {
bigint,
boolean,
integer,
json,
@@ -20,7 +21,6 @@ import { gitlab } from "./gitlab";
import { mounts } from "./mount";
import { ports } from "./port";
import { previewDeployments } from "./preview-deployments";
import { projects } from "./project";
import { redirects } from "./redirects";
import { registry } from "./registry";
import { security } from "./security";
@@ -164,6 +164,7 @@ export const applications = pgTable("application", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
//
replicas: integer("replicas").default(1).notNull(),
applicationStatus: applicationStatus("applicationStatus")
@@ -312,6 +313,7 @@ const createSchema = createInsertSchema(applications, {
watchPaths: z.array(z.string()).optional(),
previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateApplication = createSchema.pick({

View File

@@ -11,7 +11,9 @@ export const bitbucket = pgTable("bitbucket", {
.primaryKey()
.$defaultFn(() => nanoid()),
bitbucketUsername: text("bitbucketUsername"),
bitbucketEmail: text("bitbucketEmail"),
appPassword: text("appPassword"),
apiToken: text("apiToken"),
bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
gitProviderId: text("gitProviderId")
.notNull()
@@ -29,7 +31,9 @@ const createSchema = createInsertSchema(bitbucket);
export const apiCreateBitbucket = createSchema.extend({
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
appPassword: z.string().optional(),
apiToken: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
gitProviderId: z.string().optional(),
authId: z.string().min(1),
@@ -46,9 +50,19 @@ export const apiBitbucketTestConnection = createSchema
.extend({
bitbucketId: z.string().min(1),
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
workspaceName: z.string().optional(),
apiToken: z.string().optional(),
appPassword: z.string().optional(),
})
.pick({ bitbucketId: true, bitbucketUsername: true, workspaceName: true });
.pick({
bitbucketId: true,
bitbucketUsername: true,
bitbucketEmail: true,
workspaceName: true,
apiToken: true,
appPassword: true,
});
export const apiFindBitbucketBranches = z.object({
owner: z.string(),
@@ -60,6 +74,9 @@ export const apiUpdateBitbucket = createSchema.extend({
bitbucketId: z.string().min(1),
name: z.string().min(1),
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
appPassword: z.string().optional(),
apiToken: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
organizationId: z.string().optional(),
});

View File

@@ -21,6 +21,7 @@ export const deploymentStatus = pgEnum("deploymentStatus", [
"running",
"done",
"error",
"cancelled",
]);
export const deployments = pgTable("deployment", {

View File

@@ -58,4 +58,5 @@ export const apiUpdateGithub = createSchema.extend({
githubId: z.string().min(1),
name: z.string().min(1),
gitProviderId: z.string().min(1),
githubAppName: z.string().min(1),
});

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -62,6 +62,7 @@ export const mariadb = pgTable("mariadb", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -128,6 +129,7 @@ const createSchema = createInsertSchema(mariadb, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateMariaDB = createSchema

View File

@@ -1,5 +1,12 @@
import { relations } from "drizzle-orm";
import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import {
bigint,
boolean,
integer,
json,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -58,6 +65,7 @@ export const mongo = pgTable("mongo", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -118,6 +126,7 @@ const createSchema = createInsertSchema(mongo, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateMongo = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -60,6 +60,7 @@ export const mysql = pgTable("mysql", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -125,6 +126,7 @@ const createSchema = createInsertSchema(mysql, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateMySql = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -60,6 +60,7 @@ export const postgres = pgTable("postgres", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -118,6 +119,7 @@ const createSchema = createInsertSchema(postgres, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreatePostgres = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -60,6 +60,7 @@ export const redis = pgTable("redis", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
environmentId: text("environmentId")
@@ -108,6 +109,7 @@ const createSchema = createInsertSchema(redis, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateRedis = createSchema

View File

@@ -186,6 +186,8 @@ export const apiAssignPermissions = createSchema
canAccessToAPI: z.boolean().optional(),
canAccessToSSHKeys: z.boolean().optional(),
canAccessToGitProviders: z.boolean().optional(),
canDeleteEnvironments: z.boolean().optional(),
canCreateEnvironments: z.boolean().optional(),
})
.required();
@@ -322,6 +324,11 @@ export const apiUpdateWebServerMonitoring = z.object({
});
export const apiUpdateUser = createSchema.partial().extend({
email: z
.string()
.email("Please enter a valid email address")
.min(1, "Email is required")
.optional(),
password: z.string().optional(),
currentPassword: z.string().optional(),
name: z.string().optional(),

View File

@@ -68,6 +68,7 @@ export * from "./utils/backups/postgres";
export * from "./utils/backups/utils";
export * from "./utils/backups/web-server";
export * from "./utils/builders/compose";
export * from "./utils/startup/cancell-deployments";
export * from "./utils/builders/docker-file";
export * from "./utils/builders/drop";
export * from "./utils/builders/heroku";

View File

@@ -92,31 +92,48 @@ export const suggestVariants = async ({
const { object } = await generateObject({
model,
output: "array",
output: "object",
schema: z.object({
id: z.string(),
name: z.string(),
shortDescription: z.string(),
description: z.string(),
suggestions: z.array(
z.object({
id: z.string(),
name: z.string(),
shortDescription: z.string(),
description: z.string(),
}),
),
}),
prompt: `
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items), the suggestion
should include id, name, shortDescription, and description. Use slug of title for id.
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
Return your response as a JSON object with the following structure:
{
"suggestions": [
{
"id": "project-slug",
"name": "Project Name",
"shortDescription": "Brief one-line description",
"description": "Detailed description"
}
]
}
Important rules for the response:
1. The description field should ONLY contain a plain text description of the project, its features, and use cases
2. Do NOT include any code snippets, configuration examples, or installation instructions in the description
3. The shortDescription should be a single-line summary focusing on the main technologies
1. Use slug format for the id field (lowercase, hyphenated)
2. The description field should ONLY contain a plain text description of the project, its features, and use cases
3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
4. The shortDescription should be a single-line summary focusing on the main technologies
5. All projects should be installable in docker and have docker compose support
User wants to create a new project with the following details, it should be installable in docker and can be docker compose generated for it:
User wants to create a new project with the following details:
${input}
`,
});
if (object?.length) {
if (object?.suggestions?.length) {
const result = [];
for (const suggestion of object) {
for (const suggestion of object.suggestions) {
try {
const { object: docker } = await generateObject({
model,
@@ -136,16 +153,29 @@ export const suggestVariants = async ({
serviceName: z.string(),
}),
),
configFiles: z.array(
z.object({
content: z.string(),
filePath: z.string(),
}),
),
configFiles: z
.array(
z.object({
content: z.string(),
filePath: z.string(),
}),
)
.optional(),
}),
prompt: `
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
Return the docker compose as a YAML string and environment variables configuration. Follow these rules:
Return your response as a JSON object with this structure:
{
"dockerCompose": "yaml string here",
"envVariables": [{"name": "VAR_NAME", "value": "example_value"}],
"domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}],
"configFiles": [{"content": "file content", "filePath": "path/to/file"}]
}
Note: configFiles is optional - only include it if configuration files are absolutely required.
Follow these rules:
Docker Compose Rules:
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml
@@ -198,6 +228,7 @@ export const suggestVariants = async ({
console.error("Error in docker compose generation:", error);
}
}
return result;
}

View File

@@ -68,10 +68,26 @@ export const updateBitbucket = async (
input: typeof apiUpdateBitbucket._type,
) => {
return await db.transaction(async (tx) => {
// First get the current bitbucket provider to get gitProviderId
const currentProvider = await tx.query.bitbucket.findFirst({
where: eq(bitbucket.bitbucketId, bitbucketId),
});
if (!currentProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket provider not found",
});
}
const result = await tx
.update(bitbucket)
.set({
...input,
bitbucketUsername: input.bitbucketUsername,
bitbucketEmail: input.bitbucketEmail,
appPassword: input.appPassword,
apiToken: input.apiToken,
bitbucketWorkspaceName: input.bitbucketWorkspaceName,
})
.where(eq(bitbucket.bitbucketId, bitbucketId))
.returning();
@@ -83,7 +99,7 @@ export const updateBitbucket = async (
name: input.name,
organizationId: input.organizationId,
})
.where(eq(gitProvider.gitProviderId, input.gitProviderId))
.where(eq(gitProvider.gitProviderId, currentProvider.gitProviderId))
.returning();
}

View File

@@ -603,6 +603,21 @@ const BUNNY_CDN_IPS = new Set([
"89.187.184.176",
]);
// Arvancloud IP ranges
// https://www.arvancloud.ir/fa/ips.txt
const ARVANCLOUD_IP_RANGES = [
"185.143.232.0/22",
"188.229.116.16/29",
"94.101.182.0/27",
"2.144.3.128/28",
"89.45.48.64/28",
"37.32.16.0/27",
"37.32.17.0/27",
"37.32.18.0/27",
"37.32.19.0/27",
"185.215.232.0/22",
];
const CDN_PROVIDERS: CDNProvider[] = [
{
name: "cloudflare",
@@ -627,6 +642,14 @@ const CDN_PROVIDERS: CDNProvider[] = [
warningMessage:
"Domain is behind Fastly - actual IP is masked by CDN proxy",
},
{
name: "arvancloud",
displayName: "Arvancloud",
checkIp: (ip: string) =>
ARVANCLOUD_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
warningMessage:
"Domain is behind Arvancloud - actual IP is masked by CDN proxy",
},
];
export const detectCDNProvider = (ip: string): CDNProvider | null => {

View File

@@ -9,7 +9,7 @@ import {
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import { stringify } from "yaml";
import type { z } from "zod";
import { encodeBase64 } from "../utils/docker/utils";
import { execAsyncRemote } from "../utils/process/execAsync";
@@ -101,7 +101,7 @@ const createCertificateFiles = async (certificate: Certificate) => {
],
},
};
const yamlConfig = dump(traefikConfig);
const yamlConfig = stringify(traefikConfig);
const configFile = path.join(certDir, "certificate.yml");
if (certificate.serverId) {

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