Compare commits

...

451 Commits

Author SHA1 Message Date
Mauricio Siu
8d56544c1d Merge pull request #3844 from Dokploy/fix/improve-loading-queries
Fix/improve loading queries
2026-02-28 23:06:59 -06:00
Mauricio Siu
ca527ab6ff test: add mock implementation for member.findMany in application command and real tests 2026-02-28 22:59:24 -06:00
Mauricio Siu
439fa17292 chore: bump version to v0.28.2 in package.json 2026-02-28 22:52:04 -06:00
Mauricio Siu
096c04486c refactor: increase cache TTL for trusted origins in admin service 2026-02-28 22:42:15 -06:00
Mauricio Siu
c9e1079076 refactor: remove console log from BreadcrumbSidebar component 2026-02-28 22:39:20 -06:00
Mauricio Siu
e29a86a85f refactor: optimize trusted origins retrieval and caching in auth and admin services 2026-02-28 22:33:31 -06:00
Mauricio Siu
1ba0eb0c2e Merge pull request #3835 from Dokploy/3799-bug-misleading-error---compose-file-not-found-when-no-domains-configured
refactor: simplify domain handling in Docker compose utility functions
2026-02-28 11:27:34 -06:00
Mauricio Siu
d7dc10993e refactor: simplify domain handling in Docker compose utility functions 2026-02-28 11:23:16 -06:00
Mauricio Siu
2a5d3975e8 chore: remove proprietary README.md and add database connection logic in index.ts 2026-02-28 11:14:12 -06:00
Mauricio Siu
9f3356ddb4 Merge pull request #3834 from Dokploy/3833-error-when-connecting-a-git-github-app---internal-server-error
fix: handle optional chaining for organization and user IDs in GitHub…
2026-02-28 11:07:27 -06:00
Mauricio Siu
f5674f5bf8 fix: handle optional chaining for organization and user IDs in GitHub provider setup 2026-02-28 11:06:45 -06:00
Mauricio Siu
17a617e585 refactor: standardize dialog component formatting in MariaDB, MySQL, and Redis update files 2026-02-27 14:01:44 -06:00
Mauricio Siu
f50eea9e05 Merge pull request #3826 from Dokploy/3822-renaming-redis-doesnt-close-the-update-dialog
feat: add state management for dialog visibility in MariaDB, MySQL, a…
2026-02-27 14:01:02 -06:00
Mauricio Siu
81ee8f653a feat: add state management for dialog visibility in MariaDB, MySQL, and Redis update components 2026-02-27 14:00:35 -06:00
Mauricio Siu
9507745cc0 Merge pull request #3824 from Dokploy/fix/breaking-change-api-rever4t
chore: update @dokploy/trpc-openapi to version 0.0.17 and adjust Open…
2026-02-27 13:55:48 -06:00
Mauricio Siu
d33e164876 chore: bump version to v0.28.1 in package.json 2026-02-27 13:55:18 -06:00
Mauricio Siu
7e6e815375 chore: update @dokploy/trpc-openapi to version 0.0.17 and adjust OpenAPI document URL in settings router 2026-02-27 13:54:58 -06:00
Mauricio Siu
8ab139e222 Merge pull request #3817 from Dokploy/refactor/support-only-for-startup-plan
Refactor/support only for startup plan
2026-02-27 00:20:54 -06:00
Mauricio Siu
55c04b1323 feat: add current billing plan retrieval and enhance SSH error messages with user-friendly guidance 2026-02-27 00:20:20 -06:00
Mauricio Siu
29fc8bfa97 fix: update documentation links for server setup and SSH key creation components 2026-02-27 00:11:00 -06:00
Mauricio Siu
e90d8c0ac4 Merge pull request #3777 from karyanayandi/fix/stable-composite-keys-in-mapped-lists
fix: replace index-only keys with stable composite keys in mapped lists
2026-02-26 22:53:22 -06:00
Mauricio Siu
98d09f187e Merge pull request #3816 from Dokploy/3815-in-database-backups-of-docker-compose-type-projects-the-keep-the-latest-n-function-does-not-take-effect
feat: add functionality to keep the latest N backups after running a job
2026-02-26 22:27:04 -06:00
Mauricio Siu
f96a6a509b feat: add functionality to keep the latest N backups after running a job 2026-02-26 22:26:30 -06:00
Mauricio Siu
df27b8b748 Merge pull request #3813 from Dokploy/3463-container-metrics-query-broken-after-like-change-in-3438
refactor: streamline container name processing in metric retrieval fu…
2026-02-26 21:42:40 -06:00
Mauricio Siu
5c915bc1b9 refactor: streamline container name processing in metric retrieval functions 2026-02-26 21:39:57 -06:00
Mauricio Siu
a188ed3914 Merge pull request #3812 from Dokploy/3354-requests-tab-doesnt-show-the-requests-made-to-a-newly-added-projectservice
refactor: remove unnecessary access log filters from settings configu…
2026-02-26 21:23:28 -06:00
Mauricio Siu
0094b67c13 refactor: remove unnecessary access log filters from settings configuration 2026-02-26 21:22:41 -06:00
Mauricio Siu
0b9dc3a1a2 Merge pull request #3811 from Dokploy/3332-zombie-processes-from-failed-log-cleanup
refactor: improve error handling and logging in cron job setup for ba…
2026-02-26 21:05:30 -06:00
Mauricio Siu
f8628269b9 refactor: improve error handling and logging in cron job setup for backups and log cleanup 2026-02-26 21:04:16 -06:00
Mauricio Siu
345023f090 docs: update contribution guidelines to clarify testing requirements and large feature submissions 2026-02-25 23:46:14 -06:00
Mauricio Siu
3352f7a1c9 Merge pull request #3776 from karyanayandi/fix/replace-full-lodash-imports
fix: replace full lodash imports with per-function imports
2026-02-25 23:41:16 -06:00
Mauricio Siu
9e1406e6c8 chore: update dokploy version to v0.28.0 in package.json 2026-02-25 23:40:59 -06:00
Mauricio Siu
798bfc2b92 Merge pull request #3801 from HPaulson/patch-1
style: Minor Refactor to VPS recommendation message
2026-02-25 23:39:36 -06:00
Mauricio Siu
9d07903e71 Merge pull request #3804 from Dokploy/3749-updating-the-basic-auth-account-password-did-not-update-the-traefik-middleware-configuration
feat: enhance security handling by adding refetch capability and impr…
2026-02-25 23:31:51 -06:00
Mauricio Siu
4baf77c740 feat: enhance security handling by adding refetch capability and improving error messages in security service 2026-02-25 23:30:28 -06:00
Mauricio Siu
0adfa51174 Merge pull request #3802 from Dokploy/3768-volume-backup-custom-webhook-notifications-not-working
feat: add custom notification support for volume backup status updates
2026-02-25 23:05:06 -06:00
Mauricio Siu
7a25a9f5b4 feat: add custom notification support for volume backup status updates 2026-02-25 23:04:13 -06:00
Hunter Paulson
3d81b98f48 style: Refactor VPS recommendation message for clarity 2026-02-25 23:47:06 -05:00
Mauricio Siu
9b4a37eebf Merge pull request #3800 from Dokploy/3765-missing-mounts-endpoint-for-managed-database-services
feat: enhance mount API to support service type filtering and organiz…
2026-02-25 22:09:00 -06:00
Mauricio Siu
a467920410 feat: enhance mount API to support service type filtering and organization access checks 2026-02-25 22:08:05 -06:00
Mauricio Siu
e62bb7593a fix: add early return for empty patches in generateApplyPatchesCommand to prevent unnecessary command execution 2026-02-25 22:00:40 -06:00
Mauricio Siu
4af0bdc27b Merge pull request #3796 from Dokploy/feat/add-versioning-cloud-images
chore: enhance Docker image tagging in GitHub Actions workflow to inc…
2026-02-25 01:36:55 -06:00
Mauricio Siu
b9da9e6367 chore: enhance Docker image tagging in GitHub Actions workflow to include versioning based on branch context 2026-02-25 01:36:24 -06:00
Mauricio Siu
1ea1f6b603 chore: update @dokploy/trpc-openapi to version 0.0.16 in package.json and pnpm-lock.yaml for improved dependency management 2026-02-25 01:26:12 -06:00
Mauricio Siu
677fedbbc0 Merge pull request #3783 from Dokploy/feat/add-support-for-trusted-providers
chore: update @better-auth/sso and better-auth to version 1.5.0-beta.…
2026-02-25 01:01:22 -06:00
Mauricio Siu
234862e5b6 chore: update build scripts to use rimraf for cleaning dist directory and add rimraf as a dependency in relevant package.json files 2026-02-24 23:17:14 -06:00
Mauricio Siu
b8bfee7f87 chore: add path mapping for server module in tsconfig.json to improve module resolution in the schedules app 2026-02-24 22:55:42 -06:00
Mauricio Siu
2a3ae89f5e refactor: replace global db declaration with a globalForDb object to prevent redeclaration issues in monorepos and improve connection management in development and production environments 2026-02-24 22:48:29 -06:00
Mauricio Siu
c22e006e30 refactor: rename db variable to dbConnection for improved clarity and export the connection for consistent usage across the application 2026-02-24 22:44:48 -06:00
Mauricio Siu
66ef8a0a5f refactor: update database type declaration to use a centralized Database type for improved clarity and consistency 2026-02-24 22:41:54 -06:00
Mauricio Siu
d29de8fcba chore: remove DEBUG-BUILD.md file as it is no longer needed for debugging instructions 2026-02-24 20:32:50 -06:00
Mauricio Siu
42eeade121 chore: update drizzle-orm version to 0.45.1 across all package.json files and remove catalog reference for improved dependency management 2026-02-24 19:48:42 -06:00
autofix-ci[bot]
8e893e6c64 [autofix.ci] apply automated fixes 2026-02-25 01:41:27 +00:00
Mauricio Siu
e0c5273eb3 chore: centralize drizzle-orm versioning by using a catalog reference in package.json files and update pnpm-workspace.yaml for shared dependency management 2026-02-24 19:41:04 -06:00
Mauricio Siu
a1ebb804fe refactor: reorganize imports in utils.ts to enhance clarity and maintainability 2026-02-24 19:01:44 -06:00
Mauricio Siu
b2218efce6 refactor: update database imports to use centralized db module for improved consistency across API routes and schemas 2026-02-24 19:00:30 -06:00
Mauricio Siu
b027d21589 refactor: simplify API configuration by returning links directly and moving transformer to the main return object for improved clarity 2026-02-24 18:38:26 -06:00
Mauricio Siu
2d0874d499 feat: add triggerType field to application schema for enhanced event handling 2026-02-24 18:35:26 -06:00
Mauricio Siu
e92ba584c0 refactor: utilize createInsertSchema for server API schemas to enhance consistency and clarity across create, update, and remove operations 2026-02-24 13:04:51 -06:00
Mauricio Siu
53b612534d refactor: update volume backup and environment description handling to use undefined instead of null for improved consistency; enhance error handling in AI component 2026-02-24 13:03:42 -06:00
Mauricio Siu
7b6ece0b65 refactor: streamline mount schema definitions by utilizing createSchema for improved consistency and clarity 2026-02-24 13:00:40 -06:00
Mauricio Siu
3b7dcaca7a chore: update drizzle-orm version to 0.41.0 in package.json and pnpm-lock.yaml; refactor schema definitions for improved clarity and consistency across various database schemas 2026-02-24 12:58:22 -06:00
autofix-ci[bot]
0d0383f84a [autofix.ci] apply automated fixes 2026-02-24 07:07:46 +00:00
Mauricio Siu
331b12c7d8 chore: update @dokploy/trpc-openapi to version 0.0.14 and swagger-ui-react to version 5.31.2; refactor OpenAPI document handling in swagger.tsx and settings.ts for improved compatibility and clarity 2026-02-24 01:07:23 -06:00
Mauricio Siu
7c534d62b6 chore: update dependencies in pnpm-lock.yaml and package.json, including zod to version 4.3.6, @dokploy/trpc-openapi to version 0.0.13, and @trpc packages to version 11.10.0; refactor loading state handling in application components 2026-02-23 23:50:55 -06:00
autofix-ci[bot]
df648eccf6 [autofix.ci] apply automated fixes 2026-02-23 17:11:01 +00:00
Mauricio Siu
fca10c135a chore: update zod and react-hook-form integration to use standard-schema resolver; update dependencies in pnpm-lock.yaml 2026-02-23 11:10:28 -06:00
Mauricio Siu
03969b8f45 refactor: restructure auth export to improve type safety and clarity 2026-02-23 02:43:31 -06:00
Mauricio Siu
d8e08558cc chore: update dependencies in pnpm-lock.yaml to latest versions, including hono, ai-sdk packages, and react-hook-form 2026-02-23 02:13:51 -06:00
Mauricio Siu
293ad3862a chore: update @better-auth/utils to version 0.3.1 in package.json and pnpm-lock.yaml 2026-02-23 02:06:36 -06:00
autofix-ci[bot]
6cc646c974 [autofix.ci] apply automated fixes 2026-02-23 08:00:25 +00:00
Mauricio Siu
d00ec952a9 fix: remove unnecessary console log for provider in auth.ts 2026-02-23 02:00:04 -06:00
Mauricio Siu
74461c860e chore: update @better-auth/sso and better-auth to version 1.5.0-beta.16; rename session_temp table to session and adjust related constraints; add new SQL migration for session table; implement getTrustedProviders function for dynamic provider retrieval 2026-02-23 01:58:59 -06:00
Mauricio Siu
9d09e51cf7 Merge pull request #3780 from Dokploy/3673-dokploy-fails-with-bad-request-after-webui-triggered-update-and-does-not-recover-automatically
fix: add error handling to dokploy restart notifications
2026-02-22 20:06:53 -06:00
Mauricio Siu
e3aadf1908 fix: add error handling to dokploy restart notifications 2026-02-22 20:05:58 -06:00
Yandi
69d3286aaf fix: replace index-only keys with stable composite keys in mapped lists 2026-02-22 20:34:38 +07:00
Yandi
0b1c1e8b8c fix: replace full lodash imports with per-function imports
Importing the entire lodash library adds ~70kb to the bundle. Switch to
specific imports (lodash/debounce, lodash/escapeRegExp) so only the used
functions are included.
2026-02-22 20:34:38 +07:00
Mauricio Siu
5435e1dac4 Merge pull request #3760 from jirkavrba/feat/optional-dockerfile-path
feat: Make dockerfile path optional and default to "Dockerfile"
2026-02-22 01:48:17 -06:00
Jiří Vrba
999e5fabd3 feat: Add Dockerfile as default value for the application database column 2026-02-21 11:34:40 +01:00
Jiří Vrba
6a2098d522 Remove useless defaultValue 2026-02-20 09:31:34 +01:00
Jiří Vrba
5d3e05536e feat: Make dockerfile path optional and default to "Dockerfile" 2026-02-20 09:19:20 +01:00
Mauricio Siu
3e461f642e Merge pull request #3750 from mixelburg/fix/monitoring-memory-leak
fix: cap monitoring chart data to prevent memory leak
2026-02-19 15:06:35 -06:00
Mauricio Siu
7a62f47e43 Merge pull request #3758 from Dokploy/feat/add-new-pricing
Feat/add new pricing
2026-02-19 15:01:18 -06:00
Mauricio Siu
daf700429d refactor: update billing pricing calculations and display
- Revised pricing calculations for Hobby and Startup tiers to reflect accurate annual rates with 20% discount.
- Updated comments for clarity and consistency in pricing logic.
- Adjusted billing display to show the correct annual pricing for additional servers.
2026-02-19 14:57:45 -06:00
Mauricio Siu
781bf5e116 refactor: remove commented-out code and clean up Stripe router
- Eliminated outdated comments related to legacy plan detection and current pricing calculations.
- Improved code readability by removing unnecessary comments that no longer serve a purpose.
2026-02-19 14:53:28 -06:00
autofix-ci[bot]
66190434a7 [autofix.ci] apply automated fixes 2026-02-19 20:51:00 +00:00
Mauricio Siu
2b42ef7829 refactor: simplify product ID handling and refine validation logic in Stripe router
- Updated the product ID array construction to directly include Hobby and Startup product IDs.
- Streamlined the refinement logic for server quantity validation, improving readability without altering functionality.
- Ensured consistent validation messages for the Startup plan requirements.
2026-02-19 14:50:28 -06:00
Mauricio Siu
97374f736e feat: update billing display to show monthly pricing for additional servers
- Modified the billing section to clarify that users can add more servers at the specified monthly rate.
- Added monthly pricing display for both Hobby and Startup tiers when the annual plan is selected.
- Enhanced user experience by providing clearer pricing information based on server quantity.
2026-02-19 12:45:18 -06:00
Mauricio Siu
28fc58d898 feat: enhance billing upgrade process with confirmation dialog and toast notifications
- Integrated a confirmation dialog for upgrading plans, providing users with clear details about their current and new plans.
- Added toast notifications to inform users of successful upgrades or errors during the process.
- Updated UI elements for better styling and user experience in the billing section.
2026-02-19 12:11:27 -06:00
Mauricio Siu
32a14be564 feat: enhance billing functionality with new pricing tiers and upgrade options
- Added new pricing calculations for Hobby and Startup tiers, including server quantity handling.
- Implemented upgrade functionality for users on legacy plans to switch to Hobby or Startup plans.
- Updated Stripe webhook to correctly calculate server quantities based on subscription items.
- Refactored getStripeItems utility to accommodate new billing tiers and server quantity logic.
2026-02-19 02:04:59 -06:00
Maks Pikov
e874b2c459 fix: cap monitoring chart data to prevent memory leak (#3712) 2026-02-19 07:42:08 +00:00
Mauricio Siu
660bc3cd00 Merge pull request #3591 from vtomasr5/allow-zipdrop-build-server
fix: extract ZIP drop deployment to build server when configured
2026-02-18 20:20:27 -06:00
Mauricio Siu
c98548fa51 Merge pull request #3574 from ivanpadavan/patches-impl
feat: Add patches functionality
2026-02-18 20:19:06 -06:00
Mauricio Siu
0d4d60953e test: add mock for patch service in application.command.test.ts and application.real.test.ts
- Introduced a mock for the patch service's findMany method in both test files to simulate database behavior.
- This change enhances test coverage and ensures consistency across the test suite.
2026-02-18 15:35:14 -06:00
Mauricio Siu
f7079f51de test: remove unused mock for patch service in application.command.test.ts
- Removed the mock for the patch service to streamline the test setup.
- This change enhances test clarity by eliminating unnecessary mock definitions.
2026-02-18 15:08:14 -06:00
Mauricio Siu
15a1a5d0aa test: remove unused mock for patch service in application.real.test.ts
- Removed the mock for the patch service to streamline the test setup.
- This change enhances test clarity by eliminating unnecessary mock definitions.
2026-02-18 15:03:09 -06:00
Mauricio Siu
d99e0bf4dd test: remove unused mock constants in application.real.test.ts
- Eliminated the mock constants for paths to simplify the test setup.
- This change enhances test clarity by focusing on relevant mocks and reducing unnecessary complexity.
2026-02-18 15:02:44 -06:00
Mauricio Siu
20acc8bce5 test: remove unused patch mock and deployment test in application.real.test.ts
- Eliminated the mock for the patch service and the associated deployment test case to streamline the test suite.
- This change focuses on improving test clarity and reducing complexity by removing redundant code.
2026-02-18 15:01:50 -06:00
Mauricio Siu
5ef431b9e9 test: update mock implementation in application.real.test.ts
- Replaced the mock for the patch service with a mock for db.query.patch.findMany to better simulate database behavior.
- Added type and updatedAt fields to the mock patch object for improved test accuracy.
2026-02-18 14:54:24 -06:00
Mauricio Siu
3439b758df test: add mock for patch table in application.real.test.ts
- Introduced a mock for the patch table to simulate findMany behavior in application.real.test.ts, enhancing test coverage.
2026-02-18 14:54:17 -06:00
Mauricio Siu
97f9e8ad25 test: enhance application tests with patch mock and additional paths
- Added a mock for the patch table in application.command.test.ts to simulate findMany behavior.
- Updated application.real.test.ts to include new paths for COMPOSE_PATH, SSH_PATH, and BASE_PATH for improved test coverage.
2026-02-18 14:51:28 -06:00
Mauricio Siu
5faa319b69 feat: add patch table and related constraints
- Introduced a new SQL file to create a "patch" table with a custom ENUM type "patchType" for tracking changes.
- Added foreign key constraints linking "applicationId" and "composeId" to their respective tables.
- Removed the previous SQL file that contained redundant definitions for the "patchType" and its column in the "patch" table.
2026-02-18 14:40:48 -06:00
Mauricio Siu
42e8320866 Merge branch 'canary' into patches-impl 2026-02-18 14:39:54 -06:00
Mauricio Siu
b0c6b1338d chore: update deployment command in Dockerfiles to include --legacy flag
- Modified deployment commands in Dockerfile, Dockerfile.cloud, Dockerfile.schedule, and Dockerfile.server to add the --legacy flag for compatibility during deployment.
2026-02-18 12:11:02 -06:00
Mauricio Siu
309a411718 delete: remove integration tests for patch generation and application 2026-02-18 11:55:55 -06:00
Mauricio Siu
ef65e0934e Merge branch 'canary' into patches-impl 2026-02-18 11:54:53 -06:00
Mauricio Siu
84dd6458aa Merge pull request #3743 from Dokploy/feat/migrate-to-node-24
chore: upgrade Node.js version and related dependencies to 24.4.0
2026-02-18 11:54:13 -06:00
Mauricio Siu
37e5c52cbe chore: update fast-xml-parser and strnum dependencies
- Removed fast-xml-parser version 4.3.4 and updated to version 5.3.6 in package.json and pnpm-lock.yaml.
- Updated strnum dependency from version 1.1.2 to 2.1.2 in pnpm-lock.yaml.
2026-02-18 11:53:10 -06:00
autofix-ci[bot]
49914c5d92 [autofix.ci] apply automated fixes 2026-02-18 17:48:12 +00:00
Mauricio Siu
9df4398c8f chore: upgrade pnpm version to 10.22.0 across all Dockerfiles and package.json files
- Updated pnpm version from 9.12.0 to 10.22.0 in Dockerfiles for base, cloud, schedule, and server.
- Adjusted package.json files in multiple applications to reflect the new pnpm version.
- Updated dependencies for better-auth and related packages to version 1.4.18.
2026-02-18 11:47:41 -06:00
Mauricio Siu
a83a742bf3 fix(auth): ensure error message is properly converted to string in API key validation 2026-02-18 11:23:31 -06:00
Mauricio Siu
713aa5fd58 chore: update Node.js version in documentation and workflows to 24.4.0
- Updated the recommended Node.js version in CONTRIBUTING.md to 24.4.0.
- Modified the Node.js version in the sync-openapi-docs.yml workflow to 24.4.0.
2026-02-18 11:19:04 -06:00
Mauricio Siu
b210c48eaa chore: update package versions for better-auth and related dependencies
- Updated @better-auth/sso and better-auth dependencies to version 1.5.0-beta.16 in package.json files across multiple applications.
- Incremented @better-auth/utils version to 0.3.1.
- Adjusted pnpm-lock.yaml to reflect the updated dependency versions.
2026-02-18 11:14:58 -06:00
Mauricio Siu
d70c865dc7 chore: upgrade Node.js version and related dependencies to 24.4.0
- Updated .nvmrc and Dockerfiles to use Node.js 24.4.0.
- Modified package.json files across multiple applications to reflect the new Node.js version and updated @types/node dependency.
- Updated better-auth and @better-auth/sso dependencies to version v1.5.0-beta.16 in relevant package.json files.
- Removed outdated .nvmrc file from apps/dokploy.
2026-02-18 11:10:51 -06:00
Mauricio Siu
df2221a4bd refactor(patch): enforce minimum length for ID input and streamline repo directory access
- Updated ID input validation to require a minimum length of 1 character.
- Simplified the logic for accessing repository directories by consolidating return statements and removing unnecessary error handling for missing application or compose types.
2026-02-18 11:06:57 -06:00
Mauricio Siu
831584550b Merge branch 'canary' into patches-impl 2026-02-18 02:09:57 -06:00
Mauricio Siu
c688311580 Merge pull request #3736 from Dokploy/feat/add-modify-sso-by-admin
refactor(sso): update trusted origins handling and introduce license …
2026-02-18 01:49:37 -06:00
Mauricio Siu
b9c62cc515 refactor(license-key): remove unused import and add organization owner ID retrieval
- Removed the unused import of the organization schema.
- Introduced a new import for the getOrganizationOwnerId function to enhance license validation logic.
2026-02-18 01:40:22 -06:00
Mauricio Siu
605931861b Update packages/server/src/services/proprietary/license-key.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-18 01:39:59 -06:00
Mauricio Siu
4e8d37bff7 refactor(user): remove getTrustedOrigins query from user router
- Eliminated the getTrustedOrigins query from the user router to streamline the API and improve code maintainability.
2026-02-18 01:38:04 -06:00
Mauricio Siu
be35709cea fix(auth): remove callback URL from email sign-in process on home page 2026-02-18 01:35:57 -06:00
Mauricio Siu
6c3230648a refactor(sso): update trusted origins handling and introduce license validation
- Replaced user data fetching with a dedicated query for trusted origins in SSO settings.
- Updated mutation functions to utilize the new trusted origins query.
- Introduced a new service function to validate enterprise licenses based on organization ownership.
- Enhanced SSO router to ensure trusted origins are managed by the organization owner.
- Added callback URL for email sign-in in the home page.
2026-02-18 01:34:07 -06:00
Mauricio Siu
756d276f47 feat(workflow): add PR quality check to enforce standards and prevent issues 2026-02-17 21:09:32 -06:00
Mauricio Siu
1d5ab71bd5 Merge pull request #3735 from Dokploy/fix/Command-Injection-in-/docker-container-logs-Endpoint
feat(tests): add unit tests for validation functions in docker-contai…
2026-02-17 18:17:53 -06:00
Mauricio Siu
9880c71dba refactor(validation): update isValidSearch to prevent command injection
- Enhanced the isValidSearch function to restrict allowed characters to alphanumeric, space, dot, underscore, and hyphen, preventing command injection vulnerabilities.
- Updated unit tests to reflect the new validation rules and ensure comprehensive coverage against potential injection attacks.
2026-02-17 18:17:39 -06:00
Mauricio Siu
33c3a4ed4e fix(validation): enhance isValidSearch function to restrict allowed characters
- Updated the regex in the isValidSearch function to limit valid characters, improving input validation and security against potential injection attacks.
2026-02-17 18:11:43 -06:00
Mauricio Siu
3689a82ec5 feat(tests): add unit tests for validation functions in docker-container-logs
- Introduced tests for isValidTail, isValidSince, isValidSearch, and isValidContainerId functions to ensure proper validation and security against command injection.
- Updated docker-container-logs to utilize these validation functions, enhancing input handling for WebSocket connections.
2026-02-17 18:07:30 -06:00
Mauricio Siu
b818d661fd Merge pull request #3733 from Dokploy/fix/Remote-Code-Execution-through-Path-Traversal
feat(tests): add unit tests for readValidDirectory function to valida…
2026-02-17 14:43:33 -06:00
Mauricio Siu
1302d705e7 test(drop): add security tests for traversal prevention in unzipDrop function
- Introduced a new test suite to validate that the unzipDrop function prevents writing outside the application directory, specifically addressing potential sandbox escape vulnerabilities.
- Implemented setup and teardown logic to ensure a clean test environment for each test run.
2026-02-17 14:42:52 -06:00
Mauricio Siu
685a4c0b69 refactor(drop): replace symlink entry check with dangerous node entry validation
- Updated the unzipDrop function to remove the symlink entry check and replace it with a more general validation for dangerous node entries.
- Adjusted the associated test to reflect the change in error messaging.
2026-02-17 14:31:10 -06:00
Mauricio Siu
b58f2b236f feat(tests): add unit tests for readValidDirectory function to validate path traversal logic 2026-02-17 14:22:20 -06:00
Mauricio Siu
6350a8ddd3 refactor(compose): streamline command generation in deployCompose function
- Removed redundant command initialization and ensured proper command structure for deployment.
- Enhanced clarity by consolidating command building logic, improving maintainability of the deployCompose function.
2026-02-17 11:45:11 -06:00
autofix-ci[bot]
46e1bed5e9 [autofix.ci] apply automated fixes 2026-02-17 08:28:50 +00:00
Mauricio Siu
8aba7b08cf feat(patch): implement CreateFileDialog and integrate file creation in PatchEditor
- Added CreateFileDialog component for creating new files within the dashboard.
- Integrated file creation functionality into the PatchEditor, allowing users to create files directly from the directory structure.
- Enhanced user experience with form validation and success/error notifications during file creation.
- Updated ShowPatches to display file types with badges for better clarity on patch operations.
2026-02-17 02:28:20 -06:00
Mauricio Siu
9eeac50642 feat(patch): add patchType enum and update patch schema
- Introduced a new ENUM type "patchType" with values 'create', 'update', and 'delete' to categorize patch operations.
- Updated the "patch" table schema to include a new "type" column, defaulting to 'update', ensuring better management of patch types.
- Added a new snapshot file for version 7 to reflect the updated database schema.
2026-02-17 02:09:00 -06:00
Mauricio Siu
2db4c448d4 refactor(patch): optimize patch application command generation
- Streamlined the command generation for applying patches by removing unnecessary checks for enabled status within the loop.
- Consolidated patch retrieval and filtering logic to enhance clarity and maintainability.
- Improved directory handling for file paths to ensure proper creation before applying patches, enhancing robustness.
2026-02-17 02:05:54 -06:00
Mauricio Siu
c89f2e302b refactor(patch-editor): remove repoPath dependency and streamline patch handling
- Eliminated the repoPath parameter from the PatchEditor component and related API calls, simplifying the patch management logic.
- Updated the patch retrieval and saving processes to focus on filePath and content, enhancing clarity and maintainability.
- Adjusted the handling of file content in the CodeEditor to ensure it retrieves the correct data, improving user experience.
2026-02-17 01:57:57 -06:00
Mauricio Siu
1c25ab4303 feat(patch-dialog): add EditPatchDialog component for editing patches
- Introduced the EditPatchDialog component to facilitate patch editing within the dashboard.
- Integrated the dialog into the ShowPatches component, allowing users to edit patches directly from the list view.
- Enhanced user experience with loading indicators and success/error notifications during patch updates.
- Updated the UI to ensure consistent styling and behavior across patch management features.
2026-02-17 01:47:27 -06:00
Mauricio Siu
46ac272f3f refactor(patch): unify patch retrieval and application logic
- Consolidated patch retrieval for applications and composes into a single query method, improving code clarity and reducing redundancy.
- Updated the ShowPatches component to utilize the new unified query, simplifying data fetching logic.
- Refactored patch application commands to streamline the process for both application and compose types, enhancing maintainability and consistency across the codebase.
2026-02-17 01:35:08 -06:00
Mauricio Siu
9818e3c3ba refactor(patch): simplify patch repository management and enhance git configuration handling
- Removed redundant helper functions for retrieving git configurations for applications and composes, streamlining the codebase.
- Updated the `ensurePatchRepo` function to directly handle repository cloning based on the application or compose type, improving clarity and maintainability.
- Refactored patch creation logic to eliminate unnecessary checks and streamline the process of creating patches.
- Enhanced the handling of output paths in repository cloning functions across different git providers, ensuring consistent behavior.
2026-02-17 01:11:23 -06:00
Mauricio Siu
20320639ce refactor(patch-editor): streamline patch saving and loading logic
- Removed the local state for saving status and integrated loading state directly from the mutation hook.
- Updated the saveAsPatch function to handle success and error notifications more cleanly.
- Adjusted the file content loading to ensure it retrieves the latest data, improving user experience and consistency in the PatchEditor component.
- Refactored API calls in the patch router to unify patch retrieval logic for applications and composes, enhancing maintainability.
2026-02-17 00:41:46 -06:00
Mauricio Siu
88f387dd83 refactor(patches): unify patch handling for applications and composes
- Updated the PatchEditor and ShowPatches components to accept a unified `id` and `type` prop, replacing the previous separate `applicationId` and `composeId` props.
- Refactored API calls in the patch router to handle both application and compose types, improving code clarity and maintainability.
- Enhanced the UI to provide consistent behavior for creating and managing patches across different types.
2026-02-17 00:24:27 -06:00
autofix-ci[bot]
752f90c330 [autofix.ci] apply automated fixes 2026-02-17 06:04:26 +00:00
Mauricio Siu
0fc043d0ad feat(database): add "patch" table with foreign key constraints and create snapshot for version 7 2026-02-16 23:53:31 -06:00
Mauricio Siu
13b94ed3be Merge branch 'canary' into patches-impl 2026-02-16 23:52:26 -06:00
Mauricio Siu
7747929cdf Remove unused SQL migration and associated journal entries for "0143_cute_forge". 2026-02-16 23:50:46 -06:00
Mauricio Siu
06fd561bb1 fix(deployments): remove unnecessary newline in deployment schema definition 2026-02-16 22:44:10 -06:00
Mauricio Siu
62fb117ecf Merge pull request #3042 from theo-vdml/feat/auth-add-otp-autofill
feat(auth): add autocomplete for 2FA OTP input
2026-02-16 22:35:41 -06:00
Mauricio Siu
8713d3e1aa Merge pull request #3185 from mcfdez/feat/add-delete-old-deployments
feat(deployments): add ability to delete old deployments
2026-02-16 22:31:20 -06:00
Mauricio Siu
76038f6db6 refactor(deployments): streamline deployment clearing process and remove cloud check
- Removed the cloud check from the ClearDeployments component, simplifying the logic.
- Updated the clearOldDeployments function to accept appName and serverId, enhancing its flexibility.
- Adjusted the return values in the application and compose routers to return a boolean instead of a detailed message, improving consistency.
2026-02-16 22:19:57 -06:00
Mauricio Siu
a511f4db40 refactor(deployments): unify old deployment clearing logic for applications and composes
- Renamed and consolidated the functions for clearing old deployments to a single method, `clearOldDeployments`, which now accepts an ID and type (application or compose).
- Updated the logic to filter deployments based on status and type, improving code maintainability and reducing redundancy.
2026-02-16 22:15:39 -06:00
Mauricio Siu
95a944c4e5 feat(deployments): enhance deployment deletion logic and improve error handling
- Updated the deployment deletion process to include error handling for non-existent deployments.
- Refactored the command execution to handle both remote and local execution based on server availability.
- Simplified the logic for determining deletable deployments in the ShowDeployments component.
2026-02-16 22:12:56 -06:00
Mauricio Siu
6d6cf18108 Merge branch 'canary' into feat/add-delete-old-deployments 2026-02-16 22:06:48 -06:00
Mauricio Siu
32ed0c7285 Merge pull request #2767 from aegypius/features/support-soft-serve-webhooks
feat: add support for Soft Serve webhooks
2026-02-16 21:39:51 -06:00
Mauricio Siu
923466b4fa Merge pull request #3729 from Dokploy/feat/add-teams-notification-provider
feat(notifications): add Microsoft Teams integration for notifications
2026-02-16 21:23:09 -06:00
autofix-ci[bot]
d5163322fb [autofix.ci] apply automated fixes 2026-02-17 03:17:55 +00:00
Mauricio Siu
714849883e feat(notifications): add Microsoft Teams integration for notifications
- Introduced support for Microsoft Teams notifications, including the ability to create, update, and test connections for Teams notifications.
- Updated the notification schema to include Teams as a notification type.
- Added Teams icon and UI components for handling Teams notifications in the dashboard.
- Implemented backend logic for creating and updating Teams notifications, along with necessary database schema changes.
- Enhanced existing notification functionalities to support Teams notifications across various events (e.g., build success, failure, database backups).
2026-02-16 21:16:00 -06:00
Mauricio Siu
407ce3f425 Merge pull request #3425 from mcfdez/chore/add-devcontainer
chore: add DevContainer
2026-02-16 20:51:38 -06:00
Mauricio Siu
49a189fcbf Merge pull request #3728 from Dokploy/3722-deleted-an-environment-and-all-the-services-werent-deleted
feat(environment): add service check before environment deletion
2026-02-16 20:35:52 -06:00
Mauricio Siu
7e8d3b7162 feat(environment): add service check before environment deletion
- Implemented a new function to verify if an environment has active services before allowing its deletion. This prevents accidental deletion of environments that are still in use.
2026-02-16 20:35:31 -06:00
Mauricio Siu
24010af265 Merge pull request #3727 from Dokploy/3717-error-setting-a-number-of-replicas-of-a-dokploy-app
fix(cluster): ensure Replicas value is correctly converted to number …
2026-02-16 20:32:56 -06:00
Mauricio Siu
33192ce4d1 fix(cluster): ensure Replicas value is correctly converted to number in mode-form
- Updated the mode-form component to convert the Replicas value to a number only if it is defined and not an empty string, improving data handling for Replicated mode.
2026-02-16 20:32:29 -06:00
Mauricio Siu
02a695c6af chore(dokploy): update build-next command to use webpack
- Modified the build-next script in package.json to include the --webpack flag, enhancing the build process for the Dokploy application.
2026-02-16 14:50:08 -06:00
Mauricio Siu
e5f51fd7be chore(dependencies): add esbuild override in package.json and pnpm-lock.yaml
- Introduced an override for esbuild version 0.20.2 in both package.json and pnpm-lock.yaml to ensure consistent dependency resolution.
2026-02-16 13:35:24 -06:00
Mauricio Siu
620e4c4835 Merge pull request #3718 from Dokploy/feat/remove-internationalization
chore(dependencies): update zod version across multiple packages to 3…
2026-02-16 13:23:26 -06:00
Mauricio Siu
125c23e2c0 fix(ai): update TypeScript ignore comments for deep instantiation issue
- Changed `@ts-expect-error` to `@ts-ignore` in the suggestVariants function to address TypeScript's excessively deep instantiation warning related to Zod and AI SDK output.
2026-02-16 13:16:00 -06:00
autofix-ci[bot]
51e005701d [autofix.ci] apply automated fixes 2026-02-16 18:51:10 +00:00
Mauricio Siu
c04dd63db8 chore(dependencies): update ai-sdk packages and other dependencies
- Upgraded @ai-sdk dependencies to versions 3.0.44, 3.0.30, 3.0.21, 2.0.34, 3.0.20, and 3.0.29 in package.json files for both server and dokploy.
- Updated ai package to version 6.0.86 and ai-sdk-ollama to version 3.7.0.
- Updated swagger-ui-react to version 5.31.1.
- Added a new DEBUG-BUILD.md file for debugging build issues in the server package.
- Introduced tsconfig.server.no-decl.json to manage TypeScript compilation options without declaration files.
- Modified tsconfig.json to include .next directory for TypeScript compilation.
2026-02-16 12:50:34 -06:00
Mauricio Siu
4fd06b00a0 chore(dokploy): simplify build-next command in package.json
- Removed the unnecessary --webpack flag from the build-next script, streamlining the build process.
2026-02-16 02:23:35 -06:00
autofix-ci[bot]
1f9335ad5d [autofix.ci] apply automated fixes 2026-02-16 08:16:18 +00:00
Mauricio Siu
2cd3c27ae9 refactor(dashboard): replace localization strings with static text for certificate and port labels
- Updated the WebDomain and ManageTraefikPorts components to use static placeholders and labels instead of localization functions, improving readability and simplifying the codebase.
2026-02-16 02:15:53 -06:00
autofix-ci[bot]
53ae08cec4 [autofix.ci] apply automated fixes 2026-02-16 08:10:23 +00:00
Mauricio Siu
8aab8dd2a5 chore(dependencies): update zod version across multiple packages to 3.25.76 and remove unused i18next dependencies
- Updated zod version from 3.25.32 to 3.25.76 in pnpm-lock.yaml, package.json files for api, dokploy, schedules, and server.
- Removed i18next and related localization code from the dokploy application to streamline the codebase.
2026-02-16 02:09:33 -06:00
Mauricio Siu
e8bec0ae03 fix(compose): correct command string for docker-compose execution 2026-02-15 21:57:06 -06:00
Mauricio Siu
389a69484e refactor(sidebar): streamline organization dropdown menu interactions
- Simplified the organization selection process by enhancing the dropdown menu structure.
- Improved the layout for better visibility and usability, ensuring a smoother user experience when setting default organizations and deleting them.
- Added error handling for organization actions to provide user feedback on success or failure.
2026-02-13 00:25:02 -06:00
Mauricio Siu
f656e624f7 Merge pull request #3692 from vprudnikoff/fix/stack-rm-compose-directory-dependency
fix: prevent orphaned docker stacks when compose directory is missing
2026-02-13 00:24:12 -06:00
Mauricio Siu
f5635f6645 Merge pull request #3701 from Dokploy/3690-organization-picker-not-scrollable
feat(sidebar): enhance dropdown menu styling and organization display
2026-02-13 00:19:37 -06:00
Mauricio Siu
81a04d0777 feat(sidebar): enhance dropdown menu styling and organization display
- Updated the dropdown menu to have a maximum height and added overflow handling for better usability.
- Adjusted the layout of the organizations list to ensure proper display and scrolling behavior.
2026-02-13 00:19:08 -06:00
Mauricio Siu
b63c22a7df Merge pull request #3700 from Dokploy/feat/edit-sso-providers
Feat/edit sso providers
2026-02-13 00:17:39 -06:00
Mauricio Siu
05ad6d812c Merge branch 'canary' into feat/edit-sso-providers 2026-02-13 00:17:17 -06:00
Mauricio Siu
aa579977e3 feat(auth): update trusted providers configuration to use environment variable
- Replaced database query for trusted providers with an environment variable, allowing for more flexible configuration of SSO integrations.
2026-02-13 00:16:37 -06:00
Mauricio Siu
2788323e01 feat(sso): refactor SSO provider update logic
- Changed the update mechanism for SSO providers to use a new `updateSSOProvider` function, improving code clarity and maintainability.
- Updated the payload structure for OIDC and SAML configurations to directly use the input values instead of stringifying them.
- Enhanced the overall handling of SSO provider updates within the API router.
2026-02-13 00:15:05 -06:00
Mauricio Siu
3b74425d35 Merge pull request #3699 from Dokploy/copilot/fix-discord-notification-button
Fix decoration toggle reverting to enabled in Discord/Gotify notifications
2026-02-12 23:50:33 -06:00
autofix-ci[bot]
edbc98aea7 [autofix.ci] apply automated fixes 2026-02-13 05:50:01 +00:00
Mauricio Siu
60f5ab304a feat(sso): enhance SAML provider registration and editing experience
- Added support for editing existing SAML providers, allowing users to update issuer, domains, entry point, and certificate.
- Introduced a new function to parse SAML configuration from JSON.
- Updated the UI to reflect changes in the registration dialog based on whether the user is adding or editing a provider.
- Improved user feedback with success messages tailored for registration and updates.
- Added a new column `created_at` to the `sso_provider` table for better tracking of provider creation times.
2026-02-12 23:49:27 -06:00
Mauricio Siu
8291c6d835 feat(sso): enhance OIDC provider registration and editing functionality
- Added support for editing existing OIDC providers, allowing users to update issuer, domains, client settings, and scopes.
- Introduced a new query to fetch OIDC provider details for editing.
- Updated the UI to reflect changes in the registration dialog based on whether the user is adding or editing a provider.
- Improved error handling for domain conflicts during updates.
2026-02-12 23:35:17 -06:00
copilot-swe-agent[bot]
7928d117b3 Fix Discord and Gotify decoration button reverting bug
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-02-13 04:21:31 +00:00
copilot-swe-agent[bot]
eec4e21751 Initial plan 2026-02-13 04:18:39 +00:00
v_prudnikoff
343a84d6bc fix: prevent orphaned docker stacks when compose directory is missing
`docker stack rm` was chained after `cd` with `&&`, so if the compose
directory didn't exist the stack removal command never executed. This left
orphaned Docker services with occupied ports after deletion via the API.

Also removed a duplicate `execAsync` call that always ran outside the
`if/else` block regardless of `compose.serverId`.

Fixes #3691
2026-02-12 14:53:30 +00:00
Mauricio Siu
89416fef47 Merge pull request #3685 from Dokploy/feat/add-trusted-providers-dinamically
feat(auth): dynamically add trusted providers for account linking
2026-02-10 23:48:13 -06:00
Mauricio Siu
74d72f1494 feat(auth): dynamically add trusted providers for account linking
- Updated the account linking configuration to include trusted providers fetched from the database, enhancing flexibility in managing SSO integrations.
2026-02-10 23:47:21 -06:00
Mauricio Siu
a24dbe365a Merge pull request #3684 from Dokploy/fix/add-punycode
feat(traefik): add support for internationalized domain names (IDN)
2026-02-10 22:49:42 -06:00
Mauricio Siu
3b753ecfbf test(traefik): add tests for punycode conversion of Russian IDNs
- Added tests to verify the conversion of Russian Cyrillic domains and subdomains with IDN TLDs to punycode format, ensuring proper handling in router configurations.
- Confirmed that non-ASCII parts are correctly converted while ASCII parts remain unchanged.
2026-02-10 22:44:17 -06:00
Mauricio Siu
7184b7d4b2 feat(traefik): add support for internationalized domain names (IDN)
- Implemented a function to convert IDNs to ASCII punycode format, ensuring compatibility with Traefik requirements.
- Added tests to verify the conversion of IDNs and the handling of ASCII domains in router configurations.
2026-02-10 22:42:44 -06:00
Mauricio Siu
5c36ca3986 Merge pull request #3683 from Dokploy/3667-dokploy-update-from-ui-doesnt-work-but-states-success
fix(update-server): display release tag conditionally in server versi…
2026-02-10 18:43:22 -06:00
Mauricio Siu
3a3f3ab7d4 fix(update-server): display release tag conditionally in server version info
- Updated the server version display to conditionally show the release tag when it is either "canary" or "feature", enhancing clarity for users.
2026-02-10 18:40:53 -06:00
Mauricio Siu
1779a8a950 chore(package): bump version to v0.27.1 2026-02-10 18:35:04 -06:00
Mauricio Siu
a51a4b3e87 Merge pull request #3681 from Dokploy/3672-misleading-error-when-renaming-service-domain-still-bound-to-old-service-name
fix(docker): improve error messages for missing service names in doma…
2026-02-10 18:03:56 -06:00
Mauricio Siu
034d55d7cb fix(docker): improve error messages for missing service names in domain configuration
- Enhanced error handling in the addDomainToCompose function to provide more descriptive messages when a domain's service name is missing or when the service does not exist in the compose configuration. This improves debugging and user feedback.
2026-02-10 18:03:29 -06:00
Mauricio Siu
eeb7f00d05 Merge pull request #3680 from Dokploy/feat/add-trusted-origins-sso
Feat/add trusted origins sso
2026-02-10 18:01:17 -06:00
autofix-ci[bot]
1326d14a00 [autofix.ci] apply automated fixes 2026-02-10 23:59:10 +00:00
Mauricio Siu
59f843f8a0 fix(stripe): filter products to include only monthly and annual subscriptions
- Updated the Stripe API response to return only the monthly and annual subscription products.
- Enhanced the product listing logic to filter out unnecessary products, improving data handling in the application.
2026-02-10 17:55:50 -06:00
Mauricio Siu
fe807ae2a6 feat(sso): implement management for trusted origins in SSO settings
- Added functionality to add, edit, and remove trusted origins for SSO callbacks.
- Introduced new API mutations for managing trusted origins.
- Enhanced the SSO settings UI to include a dialog for managing trusted origins, with appropriate state handling and user feedback via toast notifications.
2026-02-10 17:52:41 -06:00
user
a4eb0bfea1 migration 2026-02-10 23:45:34 +03:00
user
ce9ba60902 impl 2026-02-10 23:43:34 +03:00
Mauricio Siu
744ebab15a refactor(deployments): enhance deployment worker and queue handling for cloud environments
- Refactored the deployment worker to create a no-op worker when Redis is disabled (e.g., IS_CLOUD), preventing BullMQ connection errors.
- Updated queue initialization to use a no-op queue in cloud environments, ensuring compatibility and stability.
- Improved error handling and logging for job processing in the deployment worker.
2026-02-10 03:11:33 -06:00
Mauricio Siu
17da1d5b3c fix: Update LICENSE_KEY_URL for production environment
- Changed the production license key URL from "https://licenses.dokploy.com" to "https://licenses-api.dokploy.com" for improved API access.
2026-02-10 00:31:40 -06:00
Mauricio Siu
f7613d9375 Merge pull request #3664 from AlexDev404/fix/break-project-description-properly
fix: Update text breaking so that it breaks words properly
2026-02-10 00:14:36 -06:00
Mauricio Siu
a43ad106f2 Merge pull request #3665 from Dokploy/3652-fixrepository-loading
refactor(dokploy): improve repository selection UI for version contro…
2026-02-10 00:13:52 -06:00
autofix-ci[bot]
0e26c5023b [autofix.ci] apply automated fixes 2026-02-10 06:13:46 +00:00
autofix-ci[bot]
f4a4530481 [autofix.ci] apply automated fixes 2026-02-10 06:12:28 +00:00
Mauricio Siu
00dc3fae11 refactor(dokploy): improve repository selection UI for version control providers
- Updated repository selection logic across Bitbucket, Gitea, GitHub, and GitLab components to display a placeholder when no repository is selected.
- Enhanced loading state messages for better user experience, ensuring users are prompted to select an account before loading repositories.
- Cleaned up conditional rendering for loading states and account selection prompts in the UI.
2026-02-10 00:11:39 -06:00
Mauricio Siu
1da23f8888 Merge pull request #3650 from Dokploy/feat/add-linking-accounts-cloud-version
Feat/add linking accounts cloud version
2026-02-09 13:29:12 -06:00
Immanuel Daviel A. Garcia
bee4e4639c amend: Apply the proper fix 2026-02-09 13:10:50 -06:00
Immanuel Daviel A. Garcia
bd5b27ad51 fix: Update text breaking so that it breaks words properly 2026-02-09 12:48:28 -06:00
Mauricio Siu
b391abfd5c feat(dokploy): add product IDs for monthly and annual subscriptions in Stripe integration
- Introduced PRODUCT_MONTHLY_ID and PRODUCT_ANNUAL_ID constants to manage subscription product IDs.
- Updated the Stripe API call to fetch only the specified subscription products, enhancing performance and clarity in product management.
2026-02-09 02:42:15 -06:00
autofix-ci[bot]
21a6657e00 [autofix.ci] apply automated fixes 2026-02-09 08:33:23 +00:00
Mauricio Siu
d348ad5556 fix(dokploy): remove console logs from linking account component
- Eliminated unnecessary console log statements in the LinkingAccount component to clean up the code and improve performance.
- Ensured that the account listing functionality remains intact while enhancing code readability.
2026-02-09 02:21:37 -06:00
Mauricio Siu
5d8b7b9b99 feat(dokploy): implement linking account feature for social providers
- Added a new component for linking Google and GitHub accounts to user profiles.
- Integrated account linking functionality with the authentication client, allowing users to link and unlink their social accounts.
- Updated the profile settings page to conditionally display the linking account component based on cloud settings.
- Enhanced error handling and loading states for a better user experience.
2026-02-09 02:21:20 -06:00
Mauricio Siu
f5fa39b97e refactor(dokploy): restrict license key access to owners only and enhance validation
- Updated the license key settings to ensure only users with the "owner" role can access certain functionalities.
- Modified the license key activation input validation to require a non-empty string.
- Improved error handling for network issues when validating license keys, providing clearer feedback to users.
- Adjusted the dashboard settings to redirect non-owner users appropriately.
2026-02-09 01:15:35 -06:00
Mauricio Siu
0a3a90c4e9 Merge pull request #3541 from gentslava/feat/docker-compose-pull
Update docker-compose command to always pull images (reopened)
2026-02-09 00:18:14 -06:00
Mauricio Siu
f440df343a Merge pull request #3593 from fernandogiacomino/canary
Replace logo.svg with updated SVG design
2026-02-09 00:14:26 -06:00
Mauricio Siu
4ec282b2f3 Merge pull request #3648 from Dokploy/ulimits-at-0a401843
Ulimits at 0a401843
2026-02-08 23:40:40 -06:00
Mauricio Siu
c039e638a6 refactor(dokploy): reorganize imports and simplify ulimitsSwarm assignment
- Moved the Tooltip imports to a more appropriate location for better readability.
- Simplified the assignment of ulimitsSwarm to ensure it directly accesses the data property.
2026-02-08 23:36:20 -06:00
Mauricio Siu
65ffc63da4 feat(dokploy): add ulimitsSwarm column to multiple database tables and update journal
- Introduced a new column "ulimitsSwarm" of type json to the "application", "mariadb", "mongo", "mysql", "postgres", and "redis" tables.
- Added a corresponding entry in the journal for version 7 to track this migration.
2026-02-08 23:31:22 -06:00
Mauricio Siu
5ba120567f Merge branch 'canary' into ulimits-at-0a401843 2026-02-08 23:30:14 -06:00
Mauricio Siu
8a335789b3 chore: remove deprecated SQL migration and associated journal entry for ulimits configuration 2026-02-08 23:30:05 -06:00
Mauricio Siu
d420311507 docs: Update CONTRIBUTING.md and pull request template for clarity
- Corrected a typo in the CONTRIBUTING.md file, changing "comunity" to "community."
- Added a new section in CONTRIBUTING.md titled "Important Considerations for Pull Requests" to emphasize the necessity of testing before submission.
- Enhanced the pull request template to remind contributors to test their changes locally before submitting, ensuring a smoother review process.
2026-02-08 23:13:19 -06:00
Mauricio Siu
a01ace12e8 Merge pull request #3434 from bdkopen/update-subdependencies
chore: Resolve CVEs by Updating `swagger-react-ui` & `octokit` Sub-dependencies
2026-02-08 21:01:24 -06:00
Mauricio Siu
24c022f837 Merge pull request #3647 from Dokploy/2659-upgrading-dokploy-admin-resulted-in-bad-gateway-500-consistently
feat(dokploy): add wait-for-postgres script and update Dockerfile and…
2026-02-08 16:22:47 -06:00
Mauricio Siu
ecd81eb7fa fix(dokploy): remove wait-for-postgres script from start command in package.json
- Updated the start script in package.json to eliminate the wait-for-postgres script, streamlining the application startup process.
- This change ensures that the migration and server processes are initiated directly without the wait-for-postgres dependency.
2026-02-08 16:22:26 -06:00
Mauricio Siu
f2e4a96154 feat(dokploy): add wait-for-postgres script and update Dockerfile and package.json
- Introduced a new script to wait for PostgreSQL to be ready before starting the application.
- Updated the Dockerfile to include a health check for the application.
- Modified the start script in package.json to run the wait-for-postgres script prior to starting the server and migration processes.
- Added the wait-for-postgres TypeScript file to handle connection retries to the PostgreSQL database.
2026-02-08 15:43:58 -06:00
Mauricio Siu
08ba24c252 fix(auth): update BETTER_AUTH_SECRET default value for legacy support
- Changed the default value of BETTER_AUTH_SECRET to ensure compatibility for users who enabled 2FA before the introduction of the new secret.
- This update maintains existing authentication functionality while transitioning to a more secure default.

close https://github.com/Dokploy/dokploy/issues/3645
2026-02-08 13:32:37 -06:00
Mauricio Siu
ff55270b52 refactor(auth): conditionally apply advanced cookie settings based on cloud environment
- Updated the authentication configuration to conditionally include advanced cookie settings only when not in a cloud environment.
- This change enhances flexibility in cookie management while maintaining existing security practices.
2026-02-08 04:16:03 -06:00
Mauricio Siu
f78819d81a feat(auth): add advanced cookie settings for better security management
- Introduced advanced cookie settings in the authentication configuration, including options for secure cookies and default cookie attributes.
- This enhancement aims to improve security practices related to cookie handling in the application.
2026-02-08 04:02:04 -06:00
Mauricio Siu
79e02483ad Merge pull request #3643 from Dokploy/fix/add-migration-command-on-start
feat(migration): add migration entry point and update start script
2026-02-08 03:19:48 -06:00
Mauricio Siu
f25ed46dbc feat(migration): add migration entry point and update start script
- Added a new entry point for migration in the esbuild configuration.
- Updated the start script in package.json to run the migration before starting the server.
- Removed the direct migration call from the server initialization process to streamline the workflow.
2026-02-08 03:18:44 -06:00
Mauricio Siu
7ad09c0d0d Merge pull request #3642 from Dokploy/3469-multiple-preview-deployments-regression-in-v026x-same-as-1198
3469 multiple preview deployments regression in v026x same as 1198
2026-02-08 01:53:41 -06:00
Mauricio Siu
a212d42495 refactor(database): remove unique constraint and related files for preview_deployments
- Deleted the SQL file that added a unique constraint on the combination of applicationId and pullRequestId in the preview_deployments table.
- Removed the corresponding entry from the _journal.json to reflect the deletion of the migration.
- Deleted the snapshot file for version 142, which is no longer needed.
- Updated the preview-deployments schema to remove the unique index, reverting to the previous state.
2026-02-08 01:51:02 -06:00
Mauricio Siu
51095e3ac5 feat(database): add unique constraint to preview_deployments table and update schema
- Introduced a new SQL file to add a unique constraint on the combination of applicationId and pullRequestId in the preview_deployments table.
- Updated the _journal.json to include the new migration entry for version 142.
- Created a new snapshot file for version 142 to reflect the current database schema.
- Modified the preview-deployments schema to include a unique index for applicationId and pullRequestId, enhancing data integrity.
2026-02-08 01:28:23 -06:00
Mauricio Siu
a897fe6115 Merge pull request #3640 from Dokploy/3636-bug-build-server-env-file-creation-fails---no-such-file-or-directory-on-remote-build-server
3636 bug build server env file creation fails   no such file or directory on remote build server
2026-02-07 23:35:22 -06:00
Mauricio Siu
a0d9f06a35 fix(logs): ensure safe access to service error in ShowDockerLogs component
- Updated the ShowDockerLogs component to use optional chaining when accessing the error property of services, preventing potential runtime errors.
- Refactored the deployApplication function to create an applicationEntity object, ensuring consistent use of serverId across repository cloning functions.
- Removed unused createEnvFile function from utils, streamlining the codebase.
2026-02-07 23:34:35 -06:00
Mauricio Siu
f1d0fb95f4 feat(deployment-logs): enhance readValidDirectory function to accept serverId parameter
- Updated the readValidDirectory function to include an optional serverId parameter, improving its flexibility for directory validation.
- Modified the deployment logs WebSocket server setup to utilize the updated readValidDirectory function, ensuring proper log path validation based on server context.
2026-02-07 23:19:33 -06:00
Mauricio Siu
4bc494e009 Merge pull request #3637 from Dokploy/3392-conflict-between-daily-docker-cleanup-and-volume-backup-turn-off-container-during-backup
3392 conflict between daily docker cleanup and volume backup turn off container during backup
2026-02-07 23:03:24 -06:00
Mauricio Siu
110bdce38c feat(schedules): replace hardcoded cron schedule with CLEANUP_CRON_JOB constant
- Updated the cron schedule for Docker cleanup tasks across multiple files to use the new CLEANUP_CRON_JOB constant.
- This change enhances maintainability by centralizing the cron schedule configuration, ensuring consistency across the application.
2026-02-07 22:58:21 -06:00
Mauricio Siu
b9e700243e feat(volume-backups): implement volume backup locking mechanism
- Added a locking mechanism to prevent concurrent volume backups, ensuring data integrity during backup operations.
- Introduced a `lockWrapper` function that manages the locking process using either `flock` or directory-based locking.
- Updated the `backupVolume` function to utilize the locking mechanism for both application and compose service types, enhancing the reliability of backup processes.
2026-02-07 22:03:16 -06:00
Mauricio Siu
bc39addfa8 feat(volume-backups): enhance query to order backups by creation date
- Updated the volume backups query to include ordering by the `createdAt` field in descending order.
- This change improves the retrieval of backup records, ensuring the most recent backups are prioritized in the response.
2026-02-07 22:01:02 -06:00
Mauricio Siu
2532934cdf fix(settings): correct database query syntax in reconnectServicesToTraefik function
- Updated the database query in the `reconnectServicesToTraefik` function to remove optional chaining, ensuring proper execution of the query.
- This change enhances the reliability of service reconnections to Traefik by ensuring the database connection is correctly utilized.
2026-02-07 20:06:33 -06:00
Mauricio Siu
f4b5a589b6 Merge pull request #3635 from Dokploy/3313-traefik-loses-all-bridge-network-connections-when-modifying-port-mappings-or-toggling-dashboard
feat(traefik): implement reconnectServicesToTraefik function
2026-02-07 19:44:41 -06:00
Mauricio Siu
105562bdcb feat(traefik): implement reconnectServicesToTraefik function
- Added a new function `reconnectServicesToTraefik` to facilitate the reconnection of services to Traefik based on the server ID.
- The function queries the database for isolated deployments and constructs Docker network connect commands for each service.
- Enhanced the existing Traefik setup process by ensuring services are properly reconnected after setup.
2026-02-07 19:42:14 -06:00
Mauricio Siu
16359e21a2 Merge pull request #3624 from horsley/fix/setup-import-side-effect
fix: avoid database connection during setup by using native exec
2026-02-07 15:53:27 -06:00
Mauricio Siu
9451958193 Merge pull request #3632 from Dokploy/1591-gitea-git-provider-fails-after-the-authorize-application-stage-self-hosted-on-private-network
feat(gitea): add optional internal URL for Gitea integration
2026-02-07 13:01:37 -06:00
autofix-ci[bot]
1a810790cd [autofix.ci] apply automated fixes 2026-02-07 19:01:22 +00:00
Mauricio Siu
e426c89cb2 feat(gitea): add optional internal URL for Gitea integration
- Introduced a new field `giteaInternalUrl` in the Gitea provider settings to allow users to specify an internal URL for OAuth token exchange when Gitea runs on the same instance as Dokploy.
- Updated the Gitea provider forms to include the new field with appropriate descriptions.
- Modified the token exchange logic to utilize the internal URL if provided, enhancing connectivity options for users.
- Updated database schema to accommodate the new field.
2026-02-07 13:00:47 -06:00
Mauricio Siu
325a0aeedf Merge pull request #3631 from Dokploy/3223-dokploy-selfhosted-gitlab-ce-selfhosted-error-postgre
feat(gitlab): add optional internal URL for GitLab integration
2026-02-07 12:54:29 -06:00
autofix-ci[bot]
a8293b7b5c [autofix.ci] apply automated fixes 2026-02-07 18:52:44 +00:00
Mauricio Siu
54bd25da39 feat(gitlab): add optional internal URL for GitLab integration
- Introduced a new field `gitlabInternalUrl` in the GitLab provider settings to allow users to specify an internal URL for OAuth token exchange when GitLab runs on the same instance as Dokploy.
- Updated the GitLab provider forms to include the new field with appropriate descriptions.
- Modified the token exchange logic to utilize the internal URL if provided, enhancing connectivity options for users.
2026-02-07 12:51:45 -06:00
Mauricio Siu
e4c440b265 Merge pull request #3627 from Dokploy/3583-dokploy-traefik-config-editor-does-not-allow-valid-traefik-config-to-be-saved
feat(traefik): add option to skip YAML validation for Go templating
2026-02-07 02:17:03 -06:00
Mauricio Siu
e39f0fee77 feat(traefik): add option to skip YAML validation for Go templating
- Introduced a checkbox to skip YAML validation in both the UpdateTraefikConfig and ShowTraefikFile components, allowing users to save configurations that utilize Go templating.
- Updated the onSubmit logic to conditionally validate YAML based on the new checkbox state, enhancing flexibility for users working with dynamic configurations.
2026-02-07 02:15:17 -06:00
Mauricio Siu
5b48e45536 Merge pull request #3626 from Dokploy/3535-container-creation-fails-after-vps-restart-bind-source-path-does-not-exist
feat(logs): display error messages for containers in dashboard logs
2026-02-07 02:11:55 -06:00
autofix-ci[bot]
3a7f76e33e [autofix.ci] apply automated fixes 2026-02-07 08:11:32 +00:00
Mauricio Siu
a54c84a138 feat(logs): display error messages for containers in dashboard logs
- Added error message display for containers in both the application and stack log views when using the "swarm" option.
- Updated Docker command to include error information for containers, enhancing visibility into container issues.
2026-02-07 02:10:55 -06:00
Mauricio Siu
8ba26f01e3 Merge pull request #3625 from Dokploy/2842-deployment-stuck-with-remote-server
2842 deployment stuck with remote server
2026-02-07 01:55:45 -06:00
Mauricio Siu
9bc88eba72 Merge branch 'canary' into 2842-deployment-stuck-with-remote-server 2026-02-07 01:54:09 -06:00
Mauricio Siu
b741618251 feat(dashboard): add clean all deployment queue action
- Introduced a new action in the dashboard to clean the entire deployment queue, enhancing user control over deployment processes.
- Implemented error handling with toast notifications to inform users of the success or failure of the action.
- Updated the API to support the new clean all deployment queue functionality.
2026-02-07 01:53:52 -06:00
Mauricio Siu
c0328ab63f Merge pull request #3623 from Dokploy/3506-deployment-queue-gets-stuck-when-application-is-deleted-while-deployment-task-is-processing
feat(dependencies): update bullmq and related packages to version 5.67.3
2026-02-07 01:48:07 -06:00
Mauricio Siu
425bcf8958 fix(schedules): ensure cronSchedule is always a string when removing jobs
- Updated the job removal logic to default cronSchedule to an empty string if job.pattern is undefined, preventing potential errors during job removal.
2026-02-07 01:42:14 -06:00
Mauricio Siu
26d4058457 feat(dependencies): update bullmq and related packages to version 5.67.3
- Upgraded bullmq to version 5.67.3 in both dokploy and schedules applications.
- Added new functions to retrieve jobs by application and compose IDs in the queue setup.
- Enhanced application and compose routers to cancel jobs based on user requests.
- Updated package dependencies for ioredis and msgpackr to their latest versions.
2026-02-07 01:35:03 -06:00
Horsley Lee
ccaac28f08 fix: avoid database connection during setup by using native exec
The setup.ts script imports execAsync from @dokploy/server, which triggers
the entire package to load including lib/auth.ts. This module initializes
betterAuth() with a database connection at import time, causing the setup
script to fail with ECONNREFUSED before the database container is created.

This fix replaces the import with Node.js native promisify(exec), avoiding
the module side-effect that attempts database connection during setup.
2026-02-07 15:29:43 +08:00
Mauricio Siu
a1a348e22d feat(dashboard): enhance Traefik actions with health check integration
- Added a new health check mutation for reloading Traefik, improving user feedback during the reload process.
- Updated button states to reflect the execution status of health checks, preventing user actions during ongoing operations.
- Refactored error handling for Traefik reload to provide clearer feedback on failures.
2026-02-07 00:53:32 -06:00
Mauricio Siu
ad29bb6ec2 feat(settings): improve background execution of Traefik setup with error logging
- Updated Traefik setup calls to run in the background, allowing immediate client response.
- Added error handling to log issues during the background execution of Traefik setup for better debugging.
2026-02-07 00:51:03 -06:00
Mauricio Siu
aa2e0e81c6 feat(settings): run Traefik setup in background to prevent proxy timeouts
- Modified the Traefik setup call to execute in the background, allowing immediate response to the client.
- Added error handling to log any issues during the background execution of the Traefik setup.
2026-02-07 00:34:43 -06:00
Mauricio Siu
3750cdab44 Merge pull request #3618 from Dokploy/feat/add-ui-to-show-unhealthy-status-logs
feat(logs): enhance container status display in logs
2026-02-07 00:13:44 -06:00
Mauricio Siu
6cf448ba80 feat(logs): enhance container status display in logs
- Added support for displaying "ready" state in badge color logic.
- Updated logs display to include container status and current state in the dashboard components.
- Modified Docker command outputs to include status and current state for better visibility of container health.
2026-02-07 00:13:12 -06:00
Mauricio Siu
3e64647d0d Merge pull request #3617 from Dokploy/fix/add-missing-field-created-invitation-table
feat(database): add created_at column to invitation table and update …
2026-02-06 23:48:41 -06:00
Mauricio Siu
dde00fc380 feat(database): add created_at column to invitation table and update schema
- Introduced a new column "created_at" with a default timestamp to the "invitation" table.
- Updated the account schema to reflect this change, ensuring consistency across the database structure.
- Added corresponding metadata in the snapshot and journal files for versioning.
2026-02-06 23:48:21 -06:00
Mauricio Siu
f4ad3dae35 Merge pull request #3557 from vtomasr5/traefik-3.6.7-everywhere
fix: use the same traefik version everywhere
2026-02-06 23:32:09 -06:00
Mauricio Siu
85a8ec8ba9 Merge pull request #3616 from Dokploy/fix/add-loader-and-toast-success-when-traefik-loss-connection
feat(health-check): implement health check hook for post-mutation val…
2026-02-06 23:30:10 -06:00
autofix-ci[bot]
c68525aa59 [autofix.ci] apply automated fixes 2026-02-07 05:25:34 +00:00
Mauricio Siu
91d6365275 feat(health-check): implement health check hook for post-mutation validation
- Introduced `useHealthCheckAfterMutation` hook to perform health checks after mutations, ensuring services are ready before proceeding.
- Updated `ShowTraefikActions`, `EditTraefikEnv`, and `ManageTraefikPorts` components to utilize the new hook, enhancing user feedback and error handling during updates.
- Improved loading states by incorporating health check execution status into button states.
2026-02-06 23:24:38 -06:00
Mauricio Siu
35a7445a09 Merge pull request #3614 from Dokploy/3562-domain-enabled-services-lose-connectivity-to-other-compose-services-missing-default-network
refactor(network-service): enhance network addition logic to include …
2026-02-06 22:59:26 -06:00
Mauricio Siu
4607b15a85 refactor(network-service): enhance network addition logic to include default network
- Updated the addDokployNetworkToService function to automatically include the "default" network when adding new networks.
- Modified test cases to reflect the new behavior, ensuring no duplicates of the default network are added.
- Improved handling of network addition for both arrays and objects.
2026-02-06 22:56:27 -06:00
Mauricio Siu
4eae1a5c14 Merge pull request #3608 from Dokploy/3532-dokploy-is-not-updating
refactor(settings): update dokploy image handling during service update
2026-02-06 21:43:22 -06:00
Mauricio Siu
5381b13813 refactor(settings): remove deprecated Docker image functions
- Eliminated the getDokployImage and pullLatestRelease functions to streamline the settings service.
- Updated the code to focus on dynamic image retrieval, enhancing clarity and maintainability.
2026-02-06 21:40:53 -06:00
Mauricio Siu
66ae8e1fff Merge branch 'canary' into 3532-dokploy-is-not-updating 2026-02-06 15:45:47 -06:00
Mauricio Siu
1aa05eaa8d chore(pnpm-lock): add resend package version 6.8.0 2026-02-06 00:47:33 -06:00
Mauricio Siu
4f13c25ca2 refactor(network-form): clean up DriverOptsEntries rendering logic
- Simplified the rendering of DriverOptsEntries in the network form by consolidating the mapping logic.
- Improved formatting of example driver options in the form description for better readability.
- Ensured consistent handling of adding and removing driver options within the form.
2026-02-06 00:19:18 -06:00
autofix-ci[bot]
83599cee37 [autofix.ci] apply automated fixes 2026-02-06 06:18:48 +00:00
Mauricio Siu
1c4e95d8e3 refactor(settings): update dokploy image handling during service update
- Removed the deprecated getDokployImage function and replaced it with dynamic image retrieval based on available updates.
- The service update now checks for available updates and uses the latest version from the update data, enhancing deployment efficiency.
2026-02-06 00:16:02 -06:00
Mauricio Siu
97f1105cf4 Merge pull request #3594 from Dokploy/copilot/add-network-configuration-tab
Add missing Network configuration to Swarm Settings
2026-02-05 23:18:54 -06:00
Mauricio Siu
c65026353a feat(network-form): add DriverOptsEntries to network form schema and UI
- Introduced DriverOptsEntries to the network form schema, allowing users to specify driver options for networks.
- Updated the form UI to support adding, editing, and removing driver options dynamically.
- Adjusted the backend schema to accept driver options as a record of key-value pairs.
2026-02-05 23:18:41 -06:00
Mauricio Siu
8872dc178c Merge branch 'canary' into copilot/add-network-configuration-tab 2026-02-05 23:08:54 -06:00
Mauricio Siu
fa0c2ec5e3 Merge pull request #3582 from bdkopen/update-hono
chore: update `hono` to resolve 9 CVEs
2026-02-05 23:07:40 -06:00
bdkopen
f9eda8e95d chore: update axios subdependency
Resolves https://github.com/advisories/GHSA-4hjh-wcwx-xvwj
2026-02-05 21:07:54 -05:00
bdkopen
5b2b0db686 chore: update jws subdependency
Resolves https://github.com/advisories/GHSA-869p-cjfg-cm3x
2026-02-05 21:07:53 -05:00
bdkopen
6576731842 chore: update form-data subdependency
Resolves https://github.com/advisories/GHSA-fjxv-7rqg-78g4
2026-02-05 21:07:52 -05:00
bdkopen
ec7bf9fd2f chore: update sha.js subdependency
Resolves https://github.com/browserify/sha.js/security/advisories/GHSA-95m3-7q98-8xr5
2026-02-05 21:07:46 -05:00
bdkopen
bc053744fc chore: update hono to resolve 9 CVEs
- (high) https://github.com/advisories/GHSA-m732-5p4w-x69g
- (high) https://github.com/advisories/GHSA-3vhc-576x-3qv4
- (high) https://github.com/advisories/GHSA-f67f-6cw9-8mq4
- (moderate) https://github.com/advisories/GHSA-92vj-g62v-jqhh
- (moderate) https://github.com/advisories/GHSA-q7jf-gf43-6x6p
- (moderate) https://github.com/advisories/GHSA-9r54-q6cx-xmh5
- (moderate) https://github.com/advisories/GHSA-w332-q679-j88p
- (moderate) https://github.com/advisories/GHSA-6wqw-2p9w-4vw4
- (moderate) https://github.com/advisories/GHSA-r354-f388-2fhh
2026-02-05 21:04:47 -05:00
Mauricio Siu
af87614cb0 Merge pull request #3606 from Dokploy/3403-volume-backup-restart-fails-for-private-registry-images-missing---with-registry-auth
refactor(backup): replace docker service scale with update command
2026-02-05 15:45:50 -06:00
Mauricio Siu
b2484da2af refactor(backup): replace docker service scale with update command
- Updated the backupVolume function to use `docker service update --replicas` instead of `docker service scale` for stopping and starting application replicas.
- This change enhances the backup process by ensuring proper handling of service updates with registry authentication.
2026-02-05 15:45:18 -06:00
Mauricio Siu
5d3c05d291 Update package.json 2026-02-05 15:14:31 -06:00
Mauricio Siu
40accfbf60 Merge pull request #3603 from Dokploy/3581-duplicating-a-database-fails-with-unique-constraint-violation-backup_appname_unique
fix(project): include appName in backup processing
2026-02-05 15:03:53 -06:00
Mauricio Siu
3f0558d077 fix(project): include appName in backup processing
- Updated backup processing logic to destructure appName from backup objects, ensuring it is available for further operations.
- This change is applied consistently across multiple sections of the project router.
2026-02-05 15:03:22 -06:00
Mauricio Siu
7ae3d7d906 Merge pull request #3512 from mhbdev/resend-provider-for-notifications
feat: add resend notification functionality
2026-02-05 15:01:10 -06:00
Mauricio Siu
6877ebe027 feat(database): add resend notification type and related table
- Introduced a new notification type 'resend' to the database.
- Created a new table 'resend' with fields for managing resend notifications.
- Updated the 'notification' table to include a foreign key reference to 'resendId'.
- Added corresponding snapshot and journal entries to reflect these changes.
2026-02-05 15:00:10 -06:00
Mauricio Siu
51e881d831 feat(database): add pushover notification type and related table
- Introduced a new notification type 'pushover' to the database.
- Created a new table 'pushover' with relevant fields for notification management.
- Updated the 'notification' table to include a foreign key reference to 'pushoverId' for enhanced notification handling.
- Added corresponding snapshot and journal entries to reflect these changes.
2026-02-05 14:54:23 -06:00
Mauricio Siu
80bbb752b6 chore: remove obsolete SQL file and associated snapshot
- Deleted the '0137_worried_shriek.sql' file as it is no longer needed.
- Updated the journal to remove references to the deleted SQL file.
- Removed the corresponding snapshot JSON file to maintain a tidy project structure.
2026-02-05 14:53:23 -06:00
Mauricio Siu
3c9945ec35 Revert "chore: remove obsolete SQL files and associated snapshots"
This reverts commit d77c562c84.

# Conflicts:
#	apps/dokploy/drizzle/meta/0137_snapshot.json
#	apps/dokploy/drizzle/meta/_journal.json
2026-02-05 14:49:01 -06:00
Mauricio Siu
4d8c358b33 chore: update pnpm-lock.yaml with new package resolutions
- Added fast-sha256@1.3.0 and uuid@10.0.0 with their respective integrity resolutions.
- Updated snapshots to include the new package entries.
2026-02-05 14:42:25 -06:00
Mauricio Siu
4b82659a48 Merge branch 'canary' into resend-provider-for-notifications 2026-02-05 14:42:14 -06:00
Mauricio Siu
d77c562c84 chore: remove obsolete SQL files and associated snapshots
- Deleted SQL files '0136_tidy_puff_adder.sql' and '0137_worried_shriek.sql' as they are no longer needed.
- Updated the journal and removed references to the deleted SQL files.
- Cleaned up the corresponding snapshot JSON files to maintain a tidy project structure.
2026-02-05 14:41:33 -06:00
Mauricio Siu
37ea75be3e chore(readme): remove sponsor section and streamline content
- Eliminated the sponsors section and related acknowledgments from the README.md to simplify the document.
- Updated the layout to focus on the core features and purpose of Dokploy.
2026-02-05 09:51:53 -06:00
Mauricio Siu
82158ed34d feat(auth): introduce BETTER_AUTH_SECRET for better authentication handling
- Added BETTER_AUTH_SECRET constant to manage authentication secret, defaulting to a predefined value if not set in the environment.
- Updated betterAuth configuration to utilize BETTER_AUTH_SECRET for enhanced security in authentication processes.
2026-02-05 09:46:30 -06:00
Mauricio Siu
9ab98c9a63 Merge branch 'canary' into copilot/add-network-configuration-tab 2026-02-05 02:31:51 -06:00
Mauricio Siu
ca7d3f8cb3 Merge pull request #3599 from Dokploy/copilot/fix-postgres-deployment-port-conflict
fix: add port conflict validation for database external ports
2026-02-05 02:31:20 -06:00
Mauricio Siu
66448ff6c2 fix: improve error handling for external port updates in database credential components
- Updated error handling in the ShowExternal*Credentials components for MariaDB, MongoDB, MySQL, PostgreSQL, and Redis to display specific error messages when saving the external port fails.
2026-02-05 02:31:02 -06:00
Mauricio Siu
31d47efb1e Merge branch 'canary' into copilot/fix-postgres-deployment-port-conflict 2026-02-05 02:28:18 -06:00
Mauricio Siu
33802f554a chore(dokploy): update build-next script to use webpack for improved performance 2026-02-05 02:27:09 -06:00
Mauricio Siu
38265fd921 Merge pull request #3600 from Dokploy/feat/introduce-license-key-pay
Feat/introduce license key pay
2026-02-05 02:19:31 -06:00
Mauricio Siu
ca2efc5c68 fix(enterprise): update LICENSE_KEY_URL for production environment
- Changed the LICENSE_KEY_URL from "https://api-license-key.dokploy.com" to "https://licenses.dokploy.com" to reflect the correct production endpoint.
2026-02-05 02:18:24 -06:00
Mauricio Siu
dfcb422294 refactor(enterprise): consolidate LICENSE_KEY_URL handling and improve license validation logic
- Moved LICENSE_KEY_URL definition to a centralized location for better maintainability.
- Updated license validation function to utilize the new LICENSE_KEY_URL import, enhancing clarity and consistency in API calls.
2026-02-05 02:15:59 -06:00
autofix-ci[bot]
47470e2343 [autofix.ci] apply automated fixes 2026-02-05 08:05:36 +00:00
Mauricio Siu
bac9dd5c31 chore(deps): update @types/node version across multiple packages
- Upgraded @types/node from version ^18.19.104 to ^20.16.0 in package.json files for apps/api, apps/dokploy, apps/schedules, and packages/server.
- Adjusted pnpm-lock.yaml to reflect the updated @types/node version across all relevant dependencies.
- Added a new setup file for mock database interactions in the dokploy app to enhance testing capabilities.
2026-02-05 02:05:07 -06:00
Mauricio Siu
65dab84e7f chore(deps): upgrade drizzle-orm and better-auth utils versions in package.json and pnpm-lock.yaml
- Updated drizzle-orm from version ^0.39.3 to ^0.41.0 for enhanced performance and features.
- Upgraded @better-auth/utils from version 0.2.4 to 0.3.0 to incorporate the latest improvements.
- Adjusted pnpm-lock.yaml to reflect these dependency updates.
2026-02-05 01:55:58 -06:00
autofix-ci[bot]
3a0da19ea8 [autofix.ci] apply automated fixes 2026-02-05 07:54:52 +00:00
Mauricio Siu
5e460e6b4f chore(deps): update drizzle-orm and drizzle-kit versions in package.json and pnpm-lock.yaml
- Upgraded drizzle-orm from version ^0.39.3 to ^0.41.0 for improved functionality and performance.
- Updated drizzle-kit from version ^0.30.6 to ^0.31.4 to ensure compatibility with the latest drizzle-orm version.
- Adjusted related dependencies in pnpm-lock.yaml to reflect these changes.
2026-02-05 01:54:24 -06:00
Mauricio Siu
9299f04f74 chore(deps): update Vitest version and related dependencies in pnpm-lock.yaml and package.json
- Upgraded Vitest from version 1.6.1 to 4.0.18 to leverage new features and improvements.
- Updated dependency versions in pnpm-lock.yaml to ensure compatibility with the latest Vitest version.
2026-02-05 01:45:44 -06:00
Mauricio Siu
2746133252 delete(tests): remove mock database setup file and update Vitest configuration
- Deleted the mock database setup file to streamline the test environment.
- Updated the Vitest configuration to remove the reference to the deleted setup file, enhancing clarity in test setup.
2026-02-05 01:17:04 -06:00
Mauricio Siu
bde192c1e7 feat(admin): handle empty member list in trusted origins retrieval
- Added a check to return an empty array if no members are found, improving the robustness of the `getTrustedOrigins` function.
2026-02-05 01:13:25 -06:00
Mauricio Siu
99646f887b feat(tests): enhance mock database with member methods for testing
- Added mock implementations for `member.findFirst` and `member.findMany` methods in the mock database setup.
- This enhancement improves the test environment by allowing more comprehensive simulation of member-related database interactions.
2026-02-05 00:59:07 -06:00
Mauricio Siu
542ccc4479 feat(sso): enhance SSO provider management and trusted origins handling
- Added logic to retrieve and delete SSO providers, ensuring proper permission checks and error handling.
- Updated user trusted origins when adding or removing SSO providers, maintaining accurate origin lists.
- Refactored trusted origins retrieval to improve clarity and efficiency in the authentication process.
- Introduced utility functions for normalizing trusted origins and converting request headers.
2026-02-05 00:55:17 -06:00
Mauricio Siu
9910c0e602 feat(db): add sso_provider table and update user schema
- Created a new table `sso_provider` with relevant fields and constraints.
- Added new columns to the `user` table: `enableEnterpriseFeatures`, `licenseKey`, `isValidEnterpriseLicense`, and `trustedOrigins`.
- Established foreign key relationships for `user_id` and `organization_id` in the `sso_provider` table.
2026-02-05 00:50:12 -06:00
Mauricio Siu
4f0d707905 delete: remove obsolete SQL migration files and snapshots
- Deleted SQL migration files for `0137_naive_power_pack`, `0138_common_mathemanic`, `0139_smiling_havok`, and `0140_great_lightspeed` as they are no longer needed.
- Removed corresponding snapshot files to maintain consistency in the database schema history.
2026-02-05 00:48:44 -06:00
copilot-swe-agent[bot]
a86fe46b7b Fix variable naming in database routers
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-02-05 05:54:23 +00:00
copilot-swe-agent[bot]
139c06b63d Add port validation to database external ports
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-02-05 05:52:38 +00:00
copilot-swe-agent[bot]
999dc7d360 Initial plan 2026-02-05 05:47:52 +00:00
Mauricio Siu
dc74d3057a fix(tests): update setup file path in Vitest configuration for clarity
- Changed the setup file path for global mocks in the Vitest configuration to a more explicit location, improving clarity and organization of test setup.
2026-02-04 23:36:00 -06:00
Mauricio Siu
ac833ef265 feat(tests): enhance Vitest configuration with additional environment variables and updated setup path
- Updated the setup file path for global mocks in the Vitest configuration to improve clarity.
- Added environment variables for GitHub and Google credentials to the test environment, facilitating integration testing.
2026-02-04 23:34:20 -06:00
Mauricio Siu
00e31f399e fix(db): update deprecation warning for legacy database credentials
- Added a condition to display the deprecation warning for legacy database credentials only in non-test environments.
- This change prevents unnecessary warnings during testing, improving the developer experience.
2026-02-04 23:33:17 -06:00
Mauricio Siu
8001e5d24a feat(tests): add values method to mock database for enhanced testing
- Introduced a mock implementation for the `values` method in the mock database setup.
- This addition improves the test environment by allowing more comprehensive simulation of database interactions.
2026-02-04 23:33:10 -06:00
Mauricio Siu
cfb9534e06 feat(tests): enhance mock database with web server settings for testing
- Added mock implementations for `webServerSettings` to support the `getWebServerSettings` function in tests.
- This update improves the test environment by simulating necessary database interactions for web server settings.
2026-02-04 23:28:51 -06:00
copilot-swe-agent[bot]
c2894260dc Revert to consistent pattern with existing swarm forms
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-02-04 15:23:29 +00:00
copilot-swe-agent[bot]
582f493f3f Fix type safety and optimize mutation payload in network form
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-02-04 15:22:26 +00:00
Mauricio Siu
8335f40238 feat(tests): add mock database setup for Vitest testing environment
- Introduced a new mock database setup file to simulate database interactions during tests.
- Updated Vitest configuration to include the mock setup file, enhancing test reliability and isolation.
2026-02-04 09:20:19 -06:00
copilot-swe-agent[bot]
f6f0921560 Add network configuration form to Swarm Settings
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-02-04 15:20:03 +00:00
copilot-swe-agent[bot]
c739c67616 Initial plan 2026-02-04 15:16:49 +00:00
Mauricio Siu
dc8148ae51 fix(db): update database URL configuration for production and development environments
- Modified the database URL assignment logic to differentiate between production and development environments.
- Ensured that the correct database URL is used based on the NODE_ENV variable, improving deployment flexibility.
2026-02-04 08:58:10 -06:00
Mauricio Siu
3307f62183 refactor(auth): remove unused SSO provider retrieval logic
- Deleted the import statement for `getSSOProviders` and the associated logic for fetching issuer origins from SSO providers.
- This cleanup improves code clarity by removing unnecessary dependencies and streamlining the trusted origins configuration.
2026-02-04 08:52:09 -06:00
Mauricio Siu
2b36381f8d fix: update import path for getPublicIpWithFallback in enterprise utility 2026-02-04 08:51:18 -06:00
Fernando Giacomino
de2579401c Replace logo.svg with updated SVG design
Updated logo.svg with polished stroke and centered space for better fit on apps.
2026-02-04 11:10:13 -03:00
autofix-ci[bot]
5c3b7acd54 [autofix.ci] apply automated fixes 2026-02-04 09:14:32 +00:00
Vicens Juan Tomas Monserrat
44f8590fe8 fix: extract ZIP drop deployment to build server when configured 2026-02-04 10:00:03 +01:00
Mauricio Siu
945406adc5 Merge branch 'canary' into feat/introduce-license-key-pay 2026-02-02 10:59:06 -06:00
Mauricio Siu
1e7522d173 Merge pull request #3580 from Bima42/fix/mistral-modal-require-bearer
fix: double bearer for mistral provider
2026-02-02 10:30:15 -06:00
Bima42
2b72b4888c fix: avoid enforce bearer for mistral in input 2026-02-02 11:45:45 +01:00
Mauricio Siu
8b1cc949c0 feat(sso): implement SSO provider retrieval functionality
- Added a new service to fetch SSO providers from the database, including relevant fields such as id, providerId, issuer, domain, oidcConfig, and samlConfig.
- This functionality will support future enhancements in SSO integration.
2026-02-01 22:44:08 -06:00
Mauricio Siu
a70018f70a feat(auth): add enterprise feature flags to user context and request validation
- Updated user context to include `enableEnterpriseFeatures` and `isValidEnterpriseLicense` properties.
- Modified request validation to set these properties based on user data, enhancing enterprise feature management.
- Adjusted the enterprise procedure to check user flags directly from the context instead of querying the database.
2026-02-01 22:01:13 -06:00
Mauricio Siu
71b87895eb refactor(auth): streamline trusted origins configuration and improve readability
- Changed the export of the `handler` and `api` constants to local scope for better clarity.
- Enhanced the trusted origins logic by restructuring the code for improved readability and maintainability.
- Commented out the cloud environment redirection logic for future consideration, aligning with previous changes in the codebase.
2026-02-01 21:38:07 -06:00
Mauricio Siu
354407cd12 chore(license): comment out cloud environment redirection logic in license settings page for future consideration 2026-02-01 20:11:19 -06:00
Mauricio Siu
766fd00be5 Merge branch 'canary' into feat/introduce-license-key-pay 2026-02-01 19:59:28 -06:00
Mauricio Siu
c31e970172 Merge pull request #3573 from bdkopen/update-next
chore: update Next to resolve 3 CVEs
2026-02-01 19:57:05 -06:00
Mauricio Siu
c56def9c97 fix(db): update database URL for Docker compatibility
- Commented out the old database URL for security reasons.
- Updated the database connection string to use the Docker service name for PostgreSQL, ensuring proper connectivity in containerized environments.
2026-02-01 19:54:43 -06:00
Mauricio Siu
aa558b3a8c feat(sso): update SAML registration dialog and settings for improved metadata handling
- Added support for IdP metadata XML in the SAML registration dialog, allowing users to paste full metadata for configuration.
- Updated the callback URL and audience handling to dynamically incorporate the base URL.
- Refactored the SSO settings to enable SAML provider registration and improved the display of callback URLs based on provider details.
- Enhanced trusted origins configuration in the authentication logic to include additional domains for development and production environments.
2026-02-01 19:50:33 -06:00
Mauricio Siu
11082f25d7 feat(sso): enhance OIDC registration mapping for Azure and other providers
- Updated the mapping logic in `register-oidc-dialog` to differentiate between Azure and other identity providers.
- Simplified the mapping structure for user attributes based on the issuer, improving flexibility in handling various OIDC providers.
2026-02-01 00:35:42 -06:00
Mauricio Siu
00ce8cad1b feat(license): enhance license key management and authorization checks
- Added authorization checks to ensure only users with the "owner" role can activate or deactivate license keys.
- Updated the menu item visibility logic to simplify role checks for admin and owner users.
- Commented out the cloud environment redirection logic in the license settings page for future consideration.
2026-01-31 18:03:03 -06:00
Mauricio Siu
dc756e2bbb refactor(auth): rename forgetPassword to requestPasswordReset for clarity
- Updated the method name from `forgetPassword` to `requestPasswordReset` in the password reset flow to better reflect its functionality.
2026-01-31 17:07:06 -06:00
Mauricio Siu
fb06cf8e55 feat(auth): add Okta domain to SSO provider list and adjust SSO request handling
- Included a new Okta domain in the array of allowed domains for SSO authentication.
- Modified the SSO request handling logic to return early if the request is an SSO callback, enhancing the flow of authentication.
2026-01-31 15:28:15 -06:00
Mauricio Siu
69ba901535 feat(sso): update SSO provider registration to handle multiple domains
- Refactored `register-oidc-dialog` and `register-saml-dialog` to accept an array of domains instead of a single domain string.
- Enhanced server-side validation to check for duplicate domains across registered providers.
- Updated SSO schema to reflect the change from a single domain to an array of domains, including validation for domain format.
2026-01-31 13:17:24 -06:00
bdkopen
4667cb525f chore: update next 2026-01-31 10:04:53 -05:00
Mauricio Siu
54229b0dcd Merge branch 'canary' into feat/introduce-license-key-pay 2026-01-31 05:16:27 -06:00
Mauricio Siu
6b42c9d142 feat(auth): expand disabled paths for SSO registration and organization management
- Added new disabled paths for organization creation, update, and deletion to enhance security in the authentication flow.
2026-01-31 05:11:45 -06:00
Mauricio Siu
7665b38b79 feat(sso): refine provider query to include user ID for enhanced security
- Updated the `listProviders` query to filter SSO providers by both organization ID and user ID.
- Modified the provider validation logic to ensure that only relevant providers are returned for the authenticated user.
2026-01-31 04:46:57 -06:00
Mauricio Siu
d5de5b8ad7 feat(sso): implement SSO provider registration and update related components
- Refactored SSO registration logic in `register-oidc-dialog` and `register-saml-dialog` to use a new mutation method.
- Removed unused imports and error handling for registration failures.
- Added foreign key constraint for `organization_id` in the `sso_provider` table.
- Introduced new SSO schema and updated user relations to include SSO providers.
- Enhanced authentication flow to support SSO provider registration.
2026-01-31 04:43:47 -06:00
Mauricio Siu
fa201a5a96 Update package.json 2026-01-31 04:35:39 -06:00
Mauricio Siu
d22d96105c feat(auth): add SSO request handling and provider validation in authentication flow 2026-01-31 03:50:54 -06:00
Mauricio Siu
bc5c65b2d2 Merge branch 'canary' into feat/introduce-license-key-pay 2026-01-31 03:44:31 -06:00
Mauricio Siu
431ad914f8 Merge pull request #3568 from Dokploy/copilot/fix-swarm-settings-test-commands
Fix swarm health check test commands not persisting
2026-01-31 03:21:20 -06:00
Mauricio Siu
0575fabb0f Merge branch 'canary' into copilot/fix-swarm-settings-test-commands 2026-01-31 03:19:29 -06:00
Mauricio Siu
385a494c83 Merge pull request #3556 from vtomasr5/fix-saving-swarm-settings-placement-preferences
fix: Save Placement button not working for Preferences in Swarm settings
2026-01-31 03:18:41 -06:00
copilot-swe-agent[bot]
d3f0bf654b Fix TypeScript type annotations in health check form
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-01-31 09:16:49 +00:00
copilot-swe-agent[bot]
9e8dacfe06 Fix health check form to properly sync test commands with form state
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-01-31 09:14:40 +00:00
copilot-swe-agent[bot]
f450b13dc5 Initial plan 2026-01-31 09:10:37 +00:00
Mauricio Siu
9e80bf45d0 Merge pull request #3567 from Dokploy/fix/security-GHSA-wmqj-wr9q-327p
feat(schema): enhance appName validation across database schemas with…
2026-01-31 03:06:56 -06:00
Mauricio Siu
a635908e43 fix(mariadb): correct appName validation to use built appName for uniqueness check 2026-01-31 03:05:08 -06:00
Mauricio Siu
960892fd8d feat(schema): enhance appName validation across database schemas with regex and message 2026-01-31 03:01:49 -06:00
Mauricio Siu
acb3c1d238 Add Sign-In Options for Cloud Users: Integrate GitHub and Google sign-in components into the registration page, allowing cloud users to register using these methods. Update UI to present alternative registration options, enhancing user experience. 2026-01-31 01:23:30 -06:00
Mauricio Siu
68587c3c8b Add SSO Provider Integration: Introduce getSSOProviders function to fetch SSO provider details from the database. Update authentication logic to include SSO domains in the server settings, enhancing SSO functionality and user experience. 2026-01-31 01:04:22 -06:00
Mauricio Siu
cae7a92599 Refactor SSO Registration Dialogs: Update RegisterOidcDialog and RegisterSamlDialog components to use field arrays for managing multiple domains and scopes. Enhance validation logic to ensure at least one domain is provided. Improve UI for adding and removing domains and scopes dynamically, streamlining the user experience in SSO configuration. 2026-01-31 00:55:09 -06:00
Mauricio Siu
f3d9960b7f Implement SSO Sign-In Options: Add components for signing in with GitHub, Google, and SSO, enhancing user authentication methods. Update SSO settings to conditionally render based on enterprise features and improve the overall login experience on the homepage. 2026-01-30 22:28:17 -06:00
Mauricio Siu
66b4bf2c4e Comment out user, session, account, verification, and apikey table definitions in auth-schema2.ts for future refactoring and cleanup. 2026-01-30 20:38:13 -06:00
Mauricio Siu
c4515a2ca4 Fix admin creation check in authentication logic: Re-enable the check for existing admin presence before creating a new admin, ensuring proper error handling for duplicate admin creation. Update cloud condition to account for admin presence. 2026-01-30 20:37:39 -06:00
autofix-ci[bot]
1f33b0fd24 [autofix.ci] apply automated fixes 2026-01-31 02:35:36 +00:00
Mauricio Siu
3c2f675eb9 Enhance SSO Functionality: Add detailed view for SSO providers in SSOSettings, including OIDC and SAML configuration parsing. Implement loading states for SSO sign-in on the homepage and expose a public API for listing SSO providers. Update UI components for better user experience and maintainability. 2026-01-30 20:35:17 -06:00
autofix-ci[bot]
61f6bbfe1c [autofix.ci] apply automated fixes 2026-01-31 02:34:32 +00:00
Vicens Juan Tomas Monserrat
21b1652259 fix: Use the same traefik version everywhere 2026-01-30 13:53:44 +01:00
Vicens Juan Tomas Monserrat
8caae549b2 fix(swarm): resolve Save Placement button not working for Preferences
The button was unresponsive because the form's flat data structure
  ({ SpreadDescriptor }) didn't match the Zod schema's nested structure
  ({ Spread: { SpreadDescriptor } }), causing silent validation failure.

  Updated schema to match form state and transform to nested structure
  only when submitting to the API.
2026-01-30 11:48:34 +01:00
Mauricio Siu
30c3e44422 Refactor SSO Registration Dialogs: Remove onSuccess prop from RegisterOidcDialog and RegisterSamlDialog components, replacing it with a call to invalidate the list of SSO providers after successful registration. Update SSOSettings to reflect these changes, enhancing the overall state management and consistency across the dialogs. 2026-01-29 22:56:25 -06:00
Mauricio Siu
f72bc28d70 Refactor enterprise backup cron job initialization: Simplified the cron job setup by consolidating user retrieval and validation logic into a single scheduled job. Updated the schedule to run every 3 days and removed redundant checks for user length. 2026-01-29 22:54:52 -06:00
Mauricio Siu
82c06a487a Remove refresh-license-validity API endpoint and integrate enterprise backup cron job initialization: Deleted the cron endpoint for refreshing license validity and added the initialization of enterprise backup cron jobs in the server setup. Updated the enterprise cron job logic to filter users based on license key and enterprise feature status. 2026-01-29 22:42:59 -06:00
Mauricio Siu
12a87f9f8b Enhance License Key Management and Enterprise Features: Update license key validation logic to ensure proper handling of enterprise licenses, including new cron job for refreshing license validity. Introduce new SQL migration for isValidEnterpriseLicense column and refactor related API procedures for better error handling and user feedback. 2026-01-29 22:37:10 -06:00
Mauricio Siu
9a8de9ae16 Add Enterprise Feature Gate Component: Introduce EnterpriseFeatureGate and EnterpriseFeatureLocked components to manage access to enterprise features based on license validation. Integrate the EnterpriseFeatureGate into the SSO settings page to conditionally render SSOSettings based on license status. 2026-01-29 22:16:23 -06:00
Mauricio Siu
6064b8ca48 Implement SAML Provider Registration and Enhance OIDC Dialog: Add a new SAML provider registration dialog with form validation using Zod, integrate it into the SSO settings page, and refactor the OIDC registration dialog to utilize React Hook Form for improved state management and validation. 2026-01-29 22:11:09 -06:00
Mauricio Siu
7f27601f7f Implement Single Sign-On (SSO) Feature: Add SSO settings page, integrate OIDC provider registration dialog, and update dependencies for better-auth to version 1.4.18. Enhance user interface with new SSO menu item and improve database schema for SSO providers. 2026-01-29 22:01:48 -06:00
Mauricio Siu
2e7f4dc1a2 Refactor License Key Settings UI: Simplify conditional rendering for license key management, update contact link to the official site, and enhance user feedback with improved loading states for activation and validation processes. 2026-01-29 08:14:35 -06:00
Mauricio Siu
2b52332e43 Enhance License Key Management: Add loading state for license key validation, implement query to check for valid license keys, and improve UI feedback during license key checks. 2026-01-29 07:58:50 -06:00
Mauricio Siu
346216fc71 Add License Settings Page: Introduce a new License settings page with server-side validation and layout integration, and update the sidebar menu to include a link for accessing the License settings. 2026-01-28 23:35:25 -06:00
Mauricio Siu
c9ffb99808 Refactor license key deactivation process: update API to retrieve the current user's license key and improve error handling for user validation and missing license keys. 2026-01-28 23:32:04 -06:00
Mauricio Siu
cbfa690a80 Improve error handling in license key management: update error logging to provide more informative messages for validation, activation, and deactivation processes. 2026-01-28 23:30:48 -06:00
Mauricio Siu
262960a59a Refactor license key management: remove legacy license key settings component, enhance license key validation and activation in the API, and implement new methods for activating and deactivating license keys. 2026-01-28 23:26:04 -06:00
Mauricio Siu
709ffddd4f Update better-auth dependency to version 1.2.8 and enhance license key validation in the API to require at least one of enableEnterpriseFeatures or licenseKey. 2026-01-28 22:50:10 -06:00
Mauricio Siu
0c299a3807 Refactor license key management: update API calls to use licenseKey router and clean up organization router by removing enterprise settings methods 2026-01-28 22:39:35 -06:00
Mauricio Siu
25fa362cdb Add enterprise features management: implement license key settings and update user schema 2026-01-28 22:34:17 -06:00
Mauricio Siu
f680818b56 Add enterprise features management: implement license key settings and update user schema 2026-01-28 11:03:00 -06:00
Mauricio Siu
20226a300c Merge pull request #3256 from luojiyin1987/fix/dockerfile-cmd-format
Fix/dockerfile cmd format
2026-01-28 09:57:07 -06:00
Mauricio Siu
5f5c4f0e18 Merge branch 'canary' into fix/dockerfile-cmd-format 2026-01-28 09:55:56 -06:00
Mauricio Siu
25d37b76a1 refactor: update swarm forms exports and remove unused SQL files
- Updated the exports in the swarm forms index to include LabelsForm and ModeForm while ensuring RestartPolicyForm and UpdateConfigForm are correctly exported.
- Removed the obsolete SQL file '0135_illegal_magik.sql' and its references from the journal.
- Deleted the associated snapshot JSON file to clean up unused database schema definitions.
2026-01-28 09:43:54 -06:00
autofix-ci[bot]
ad382f1fe5 [autofix.ci] apply automated fixes 2026-01-28 10:38:52 +00:00
mhbdev
d5b0e3193a Merge branch 'resend-provider-for-notifications' of https://github.com/mhbdev/dokploy into resend-provider-for-notifications 2026-01-28 14:08:21 +03:30
autofix-ci[bot]
43228fc51b [autofix.ci] apply automated fixes 2026-01-28 13:54:40 +03:30
mhbdev
ba8a334fbe feat: add resend notification functionality
- Introduced a new notification type "resend" to the system.
- Added database schema for resend notifications including fields for apiKey, fromAddress, and toAddress.
- Implemented functions to create, update, and send resend notifications.
- Updated notification router to handle resend notifications with appropriate API endpoints.
- Enhanced existing notification services to support sending notifications via the Resend service.
- Modified various notification utilities to accommodate the new resend functionality.
2026-01-28 13:47:57 +03:30
Vyacheslav Scherbinin
6c90075a64 feat(compose): update docker-compose command to always pull images 2026-01-28 13:35:29 +07:00
Mauricio Siu
c579dbeb1c Merge pull request #3540 from Dokploy/3491-ssl-certificate-issuance-broken-with-inwx
chore(traefik): update Traefik version to 3.6.7 in setup scripts
2026-01-28 00:18:17 -06:00
Mauricio Siu
cee1dc97ba chore(traefik): update Traefik version to 3.6.7 in setup scripts 2026-01-28 00:16:06 -06:00
Mauricio Siu
b9419ed5f1 Merge pull request #3539 from Dokploy/3493-when-adding-a-git-repository-as-a-provider-spaces-in-the-repo-name-break-the-repo-selection
feat(bitbucket): add optional slug field for repositories and update …
2026-01-28 00:14:21 -06:00
Mauricio Siu
6bc07d7675 feat(drop): add optional bitbucketRepositorySlug field to baseApp configuration in tests 2026-01-28 00:12:42 -06:00
autofix-ci[bot]
f72dfb3fc7 [autofix.ci] apply automated fixes 2026-01-28 06:10:38 +00:00
Mauricio Siu
27a0490536 feat(bitbucket): add optional slug field for repositories and update related logic 2026-01-28 00:09:56 -06:00
Mauricio Siu
ec6849205a Merge pull request #3537 from Dokploy/3510-commit-message-is-wrong-when-using-remote-builder
fix(application): update commit info extraction to include appName an…
2026-01-27 21:47:19 -06:00
Mauricio Siu
9934346d8c fix(application): update commit info extraction to include appName and serverId 2026-01-27 21:46:54 -06:00
Mauricio Siu
5c89973cc2 Merge pull request #3385 from stripsior/chore/bump-postgres
chore(databases): bump default postgres version while creating to 18
2026-01-27 21:18:50 -06:00
Mauricio Siu
4e8cdfbc80 Merge pull request #3447 from pluisol/feature/pushover-notifications
feat: add Pushover notification provider
2026-01-27 21:16:36 -06:00
autofix-ci[bot]
6271f3bb1a [autofix.ci] apply automated fixes 2026-01-24 18:15:23 +00:00
mhbdev
57eee45dbb feat: add resend notification functionality
- Introduced a new notification type "resend" to the system.
- Added database schema for resend notifications including fields for apiKey, fromAddress, and toAddress.
- Implemented functions to create, update, and send resend notifications.
- Updated notification router to handle resend notifications with appropriate API endpoints.
- Enhanced existing notification services to support sending notifications via the Resend service.
- Modified various notification utilities to accommodate the new resend functionality.
2026-01-24 21:29:19 +03:30
autofix-ci[bot]
2be92d20bb [autofix.ci] apply automated fixes 2026-01-19 08:59:46 +01:00
Marc Fernandez
2be938a695 feat: add individual deployment deletion 2026-01-19 08:59:45 +01:00
autofix-ci[bot]
95dd9ddeb6 [autofix.ci] apply automated fixes 2026-01-19 08:59:45 +01:00
Marc Fernandez
33fb21bfe1 feat: add ability to delete old deployments 2026-01-19 08:59:44 +01:00
autofix-ci[bot]
5ca4d8366e [autofix.ci] apply automated fixes 2026-01-19 08:59:20 +01:00
Marc Fernandez
cc49db63da chore: add DevContainer 2026-01-19 08:59:19 +01:00
Plui Sol
7db1f3a69a feat: add Pushover notification provider 2026-01-12 21:35:07 -05:00
Plui Sol
67f0c93298 Merge remote-tracking branch 'origin/canary' into feature/pushover-notifications 2026-01-12 21:31:48 -05:00
Plui Sol
046c52529b feat: add Pushover notification provider 2026-01-12 21:31:12 -05:00
nurikk
0a401843f8 core: add ulimits configuration for Docker Swarm deployments
Users deploying to Docker Swarm can now configure resource ulimits
(nofile, nproc, etc.) to prevent applications from hitting system
limits that cause crashes or degraded performance.
2026-01-08 21:57:58 +00:00
stripsior
27dd20b75d chore(databases): bump default postgres version while creating to 18 2026-01-03 15:16:11 +01:00
luojiyin
3142818cf2 fix(docker): use ENV for HOSTNAME and exec form CMD 2025-12-13 15:33:24 +08:00
luojiyin
d8465ac251 config: set port env 2025-12-13 12:36:15 +08:00
luojiyin
c33b41d082 fix(docker): use ENV for HOSTNAME and exec form CMD 2025-12-13 12:32:01 +08:00
luojiyin
3eea875932 code clear 2025-12-13 12:30:30 +08:00
Nicolas LAURENT
f5f21ef195 test(deployment): add Soft Serve tests 2025-11-28 16:41:40 +01:00
Nicolas LAURENT
464d58daaa feat(deployment): add support for Soft Serve webhooks 2025-11-28 16:41:40 +01:00
Théo Vandormael
50b0a5d61c feat(auth): add autocomplete for 2FA OTP input 2025-11-18 01:58:20 +01:00
469 changed files with 114694 additions and 8877 deletions

21
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# Dockerfile for DevContainer
FROM node:24.4.0-bullseye-slim
# Install essential packages
RUN apt-get update && apt-get install -y \
curl \
bash \
git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Set up PNPM
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@10.22.0 --activate
# Create workspace directory
WORKDIR /workspaces/dokploy
# Set up user permissions
USER node

View File

@@ -0,0 +1,53 @@
{
"name": "Dokploy development container",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": true,
"version": "latest"
},
"ghcr.io/devcontainers/features/git:1": {
"ppa": true,
"version": "latest"
},
"ghcr.io/devcontainers/features/go:1": {
"version": "1.20"
}
},
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.vscode-typescript-next",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-json",
"biomejs.biome",
"golang.go",
"redhat.vscode-xml",
"github.vscode-github-actions",
"github.copilot",
"github.copilot-chat"
]
}
},
"forwardPorts": [3000, 5432, 6379],
"portsAttributes": {
"3000": {
"label": "Dokploy App",
"onAutoForward": "notify"
},
"5432": {
"label": "PostgreSQL",
"onAutoForward": "silent"
},
"6379": {
"label": "Redis",
"onAutoForward": "silent"
}
},
"remoteUser": "node",
"workspaceFolder": "/workspaces/dokploy",
"runArgs": ["--name", "dokploy-devcontainer"]
}

View File

@@ -8,7 +8,7 @@ Before submitting this PR, please make sure that:
- [ ] You created a dedicated branch based on the `canary` branch.
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have tested this PR in your local instance.
- [ ] You have tested this PR in your local instance. If you have not tested it yet, please do so before submitting. This helps avoid wasting maintainers' time reviewing code that has not been verified by you.
## Issues related (if applicable)

View File

@@ -13,6 +13,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set tag and version
id: meta-cloud
run: |
VERSION=$(jq -r .version apps/dokploy/package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "tags=siumauricio/cloud:latest,siumauricio/cloud:${VERSION}" >> $GITHUB_OUTPUT
else
echo "tags=siumauricio/cloud:canary" >> $GITHUB_OUTPUT
fi
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -25,8 +36,7 @@ jobs:
context: .
file: ./Dockerfile.cloud
push: true
tags: |
siumauricio/cloud:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
tags: ${{ steps.meta-cloud.outputs.tags }}
platforms: linux/amd64
build-args: |
NEXT_PUBLIC_UMAMI_HOST=${{ secrets.NEXT_PUBLIC_UMAMI_HOST }}
@@ -40,6 +50,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set tag and version
id: meta-schedule
run: |
VERSION=$(jq -r .version apps/dokploy/package.json)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "tags=siumauricio/schedule:latest,siumauricio/schedule:${VERSION}" >> $GITHUB_OUTPUT
else
echo "tags=siumauricio/schedule:canary" >> $GITHUB_OUTPUT
fi
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -52,8 +72,7 @@ jobs:
context: .
file: ./Dockerfile.schedule
push: true
tags: |
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
tags: ${{ steps.meta-schedule.outputs.tags }}
platforms: linux/amd64
build-and-push-server-image:
@@ -63,6 +82,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set tag and version
id: meta-server
run: |
VERSION=$(jq -r .version apps/dokploy/package.json)
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "tags=siumauricio/server:latest,siumauricio/server:${VERSION}" >> $GITHUB_OUTPUT
else
echo "tags=siumauricio/server:canary" >> $GITHUB_OUTPUT
fi
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
@@ -75,6 +104,5 @@ jobs:
context: .
file: ./Dockerfile.server
push: true
tags: |
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
tags: ${{ steps.meta-server.outputs.tags }}
platforms: linux/amd64

22
.github/workflows/pr-quality.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: PR Quality
permissions:
contents: read
issues: read
pull-requests: write
on:
pull_request_target:
types: [opened, reopened]
jobs:
anti-slop:
runs-on: ubuntu-latest
steps:
- uses: peakoss/anti-slop@v0
with:
max-failures: 4
blocked-commit-authors: "claude,copilot"
require-description: true
min-account-age: 5

View File

@@ -18,7 +18,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.16.0
node-version: 24.4.0
cache: "pnpm"
- name: Install Nixpacks

View File

@@ -24,7 +24,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.16.0
node-version: 24.4.0
cache: "pnpm"
- name: Install dependencies

5
.gitignore vendored
View File

@@ -43,7 +43,4 @@ yarn-error.log*
*.pem
.db
# Development environment
.devcontainer
.db

2
.nvmrc
View File

@@ -1 +1 @@
20.16.0
24.4.0

View File

@@ -2,7 +2,7 @@
Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute.
Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues.
Before you start, please first discuss the feature/bug you want to add with the owners and community via github issues.
We have a few guidelines to follow when contributing to this project:
@@ -11,6 +11,7 @@ We have a few guidelines to follow when contributing to this project:
- [Development](#development)
- [Build](#build)
- [Pull Request](#pull-request)
- [Important Considerations](#important-considerations-for-pull-requests)
## Commit Convention
@@ -52,7 +53,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory.
We use Node v24.4.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 24.4.0 && nvm use` in the root directory.
```bash
git clone https://github.com/dokploy/dokploy.git
@@ -162,11 +163,13 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.
- If your pull request fixes an open issue, please reference the issue in the pull request description.
- Once your pull request is merged, you will be automatically added as a contributor to the project.
**Important Considerations for Pull Requests:**
### Important Considerations for Pull Requests
- **Testing is Mandatory:** All Pull Requests **must be tested** by the PR author before submission. You must verify that your changes work as expected in a local development environment (see [Setup](#setup)). **Pull Requests that have not been tested by their creator will be rejected.** This policy keeps the PR history clean and values contributors who submit verified, working code. Untested PRs are often recognizable by disproportionately large or scattered changes for simple tasks—please test first.
- **Focus and Scope:** Each Pull Request should ideally address a single, well-defined problem or introduce one new feature. This greatly facilitates review and reduces the chances of introducing unintended side effects.
- **Avoid Unfocused Changes:** Please avoid submitting Pull Requests that contain only minor changes such as whitespace adjustments, IDE-generated formatting, or removal of unused variables, unless these are part of a larger, clearly defined refactor or a dedicated "cleanup" Pull Request that addresses a specific `good first issue` or maintenance task.
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
- **Large Features:** Pull Requests that introduce very large or broad features **will not be accepted** unless the idea is first outlined and discussed in a GitHub issue. Large features should be designed together with the Dokploy team so the project stays coherent and moves in the same direction. Open an issue to propose and align on the design before implementing.
Thank you for your contribution!

View File

@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:20.16.0-slim AS base
FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@9.12.0 --activate
RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
RUN pnpm --filter=./apps/dokploy --prod deploy --legacy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
@@ -65,4 +65,8 @@ RUN curl -sSL https://railpack.com/install.sh | bash
COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
CMD [ "pnpm", "start" ]
HEALTHCHECK --interval=10s --timeout=3s --retries=10 \
CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1
CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]

View File

@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:20.16.0-slim AS base
FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@9.12.0 --activate
RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -29,7 +29,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/dokploy run build
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
RUN pnpm --filter=./apps/dokploy --prod deploy --legacy /prod/dokploy
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist

View File

@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:20.16.0-slim AS base
FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@9.12.0 --activate
RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/schedules run build
RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
RUN pnpm --filter=./apps/schedules --prod deploy --legacy /prod/schedules
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
@@ -35,4 +35,5 @@ COPY --from=build /prod/schedules/dist ./dist
COPY --from=build /prod/schedules/package.json ./package.json
COPY --from=build /prod/schedules/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start
ENV HOSTNAME=0.0.0.0
CMD ["pnpm", "start"]

View File

@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:20.16.0-slim AS base
FROM node:24.4.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@9.12.0 --activate
RUN corepack prepare pnpm@10.22.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -20,7 +20,7 @@ ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build
RUN pnpm --filter=./apps/api run build
RUN pnpm --filter=./apps/api --prod deploy /prod/api
RUN pnpm --filter=./apps/api --prod deploy --legacy /prod/api
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
@@ -35,4 +35,5 @@ COPY --from=build /prod/api/dist ./dist
COPY --from=build /prod/api/package.json ./package.json
COPY --from=build /prod/api/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start
ENV HOSTNAME=0.0.0.0
CMD ["pnpm", "start"]

View File

@@ -12,24 +12,8 @@
<br />
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://tuple.app/dokploy">
<img src=".github/sponsors/tuple.png" alt="Tuple's sponsorship image" width="400"/>
</a>
### [Tuple, the premier screen sharing app for developers](https://tuple.app/dokploy)
[Available for MacOS & Windows](https://tuple.app/dokploy)<br>
</div>
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
## ✨ Features
Dokploy includes multiple features to make your life easier.
@@ -60,40 +44,9 @@ curl -sSL https://dokploy.com/install.sh | sh
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
## ♥️ Sponsors
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
[Dokploy Open Collective](https://opencollective.com/dokploy)
[Github Sponsors](https://github.com/sponsors/Siumauricio)
## Sponsors
| Sponsor | Logo | Supporter Level |
|---------|:----:|----------------|
| [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) | <img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/> | 🎖 Hero Sponsor |
| [LX Aer](https://www.lxaer.com/?ref=dokploy) | <img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/> | 🎖 Hero Sponsor |
| [LinkDR](https://linkdr.com/?ref=dokploy) | <img src="https://dokploy.com/linkdr-logo.svg" alt="LinkDR" width="100"/> | 🎖 Hero Sponsor |
| [LambdaTest](https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor) | <img src="https://www.lambdatest.com/blue-logo.png" alt="LambdaTest" width="200"/> | 🎖 Hero Sponsor |
| [Awesome Tools](https://awesome.tools/) | <img src=".github/sponsors/awesome.png" alt="Awesome Tools" width="100"/> | 🎖 Hero Sponsor |
| [Supafort](https://supafort.com/?ref=dokploy) | <img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="200"/> | 🥇 Premium Supporter |
| [Agentdock](https://agentdock.ai/?ref=dokploy) | <img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/> | 🥇 Premium Supporter |
| [AmericanCloud](https://americancloud.com/?ref=dokploy) | <img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="200"/> | 🥈 Elite Contributor |
| [Tolgee](https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy) | <img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" width="100"/> | 🥈 Elite Contributor |
| [Cloudblast](https://cloudblast.io/?ref=dokploy) | <img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" alt="Cloudblast.io" width="150"/> | 🥉 Supporting Member |
| [Synexa](https://synexa.ai/?ref=dokploy) | <img src=".github/sponsors/synexa.png" alt="Synexa" width="100"/> | 🥉 Supporting Member |
### Community Backers 🤝
#### Organizations:
[Sponsors on Open Collective](https://opencollective.com/dokploy)
#### Individuals:
[![Individual Contributors on Open Collective](https://opencollective.com/dokploy/individuals.svg?width=890)](https://opencollective.com/dokploy)
### Contributors 🤝
<a href="https://github.com/dokploy/dokploy/graphs/contributors">

View File

@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "PORT=4000 tsx watch src/index.ts",
"build": "tsc --project tsconfig.json",
"build": "rimraf dist && tsc --project tsconfig.json",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
@@ -12,26 +12,27 @@
"inngest": "3.40.1",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.14.3",
"@hono/zod-validator": "0.3.0",
"@hono/zod-validator": "0.7.6",
"dotenv": "^16.4.5",
"hono": "^4.7.10",
"hono": "^4.11.7",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"redis": "4.7.0",
"zod": "^3.25.32"
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^20.17.51",
"@types/node": "^24.4.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"rimraf": "6.1.3",
"tsx": "^4.16.2",
"typescript": "^5.8.3"
},
"packageManager": "pnpm@9.12.0",
"packageManager": "pnpm@10.22.0",
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
"node": "^24.4.0",
"pnpm": ">=10.22.0"
}
}

View File

@@ -1 +0,0 @@
20.16.0

View File

@@ -4,21 +4,30 @@ import { describe, expect, it } from "vitest";
describe("addDokployNetworkToService", () => {
it("should add network to an empty array", () => {
const result = addDokployNetworkToService([]);
expect(result).toEqual(["dokploy-network"]);
expect(result).toEqual(["dokploy-network", "default"]);
});
it("should not add duplicate network to an array", () => {
const result = addDokployNetworkToService(["dokploy-network"]);
expect(result).toEqual(["dokploy-network"]);
expect(result).toEqual(["dokploy-network", "default"]);
});
it("should add network to an existing array with other networks", () => {
const result = addDokployNetworkToService(["other-network"]);
expect(result).toEqual(["other-network", "dokploy-network"]);
expect(result).toEqual(["other-network", "dokploy-network", "default"]);
});
it("should add network to an object if networks is an object", () => {
const result = addDokployNetworkToService({ "other-network": {} });
expect(result).toEqual({ "other-network": {}, "dokploy-network": {} });
expect(result).toEqual({
"other-network": {},
"dokploy-network": {},
default: {},
});
});
it("should not duplicate default network when already present", () => {
const result = addDokployNetworkToService(["default", "dokploy-network"]);
expect(result).toEqual(["default", "dokploy-network"]);
});
});

View File

@@ -14,13 +14,18 @@ vi.mock("@dokploy/server/db", () => {
set: vi.fn(() => chain),
where: vi.fn(() => chain),
returning: vi.fn().mockResolvedValue([{}] as any),
from: vi.fn(() => chain),
innerJoin: vi.fn(() => chain),
then: (resolve: (v: any) => void) => {
resolve([]);
},
} as any;
return chain;
};
return {
db: {
select: vi.fn(),
select: vi.fn(() => createChainableMock()),
insert: vi.fn(),
update: vi.fn(() => createChainableMock()),
delete: vi.fn(),
@@ -28,6 +33,12 @@ vi.mock("@dokploy/server/db", () => {
applications: {
findFirst: vi.fn(),
},
patch: {
findMany: vi.fn().mockResolvedValue([]),
},
member: {
findMany: vi.fn().mockResolvedValue([]),
},
},
},
};

View File

@@ -15,13 +15,18 @@ vi.mock("@dokploy/server/db", () => {
set: vi.fn(() => chain),
where: vi.fn(() => chain),
returning: vi.fn().mockResolvedValue([{}]),
from: vi.fn(() => chain),
innerJoin: vi.fn(() => chain),
then: (resolve: (v: any) => void) => {
resolve([]);
},
};
return chain;
};
return {
db: {
select: vi.fn(),
select: vi.fn(() => createChainableMock()),
insert: vi.fn(),
update: vi.fn(() => createChainableMock()),
delete: vi.fn(),
@@ -29,6 +34,12 @@ vi.mock("@dokploy/server/db", () => {
applications: {
findFirst: vi.fn(),
},
patch: {
findMany: vi.fn().mockResolvedValue([]),
},
member: {
findMany: vi.fn().mockResolvedValue([]),
},
},
},
};

View File

@@ -83,6 +83,14 @@ describe("GitHub Webhook Skip CI", () => {
{ commits: [{ message: "[skip ci] test" }] },
),
).toBe("[skip ci] test");
// Soft Serve
expect(
extractCommitMessage(
{ "x-softserve-event": "push" },
{ commits: [{ message: "[skip ci] test" }] },
),
).toBe("[skip ci] test");
});
it("should handle missing commit message", () => {
@@ -99,6 +107,9 @@ describe("GitHub Webhook Skip CI", () => {
expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe(
"NEW COMMIT",
);
expect(extractCommitMessage({ "x-softserve-event": "push" }, {})).toBe(
"NEW COMMIT",
);
});
});

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import {
extractBranchName,
extractCommitMessage,
extractHash,
getProviderByHeader,
} from "@/pages/api/deploy/[refreshToken]";
describe("Soft Serve Webhook", () => {
const mockSoftServeHeaders = {
"x-softserve-event": "push",
};
const createMockBody = (message: string, hash: string, branch: string) => ({
event: "push",
ref: `refs/heads/${branch}`,
after: hash,
commits: [{ message: message }],
});
const message: string = "feat: add new feature";
const hash: string = "3c91c24ef9560bddc695bce138bf8a7094ec3df5";
const branch: string = "feat/add-new";
const goodWebhook = createMockBody(message, hash, branch);
it("should properly extract the provider name", () => {
expect(getProviderByHeader(mockSoftServeHeaders)).toBe("soft-serve");
});
it("should properly extract the commit message", () => {
expect(extractCommitMessage(mockSoftServeHeaders, goodWebhook)).toBe(
message,
);
});
it("should properly extract hash", () => {
expect(extractHash(mockSoftServeHeaders, goodWebhook)).toBe(hash);
});
it("should properly extract branch name", () => {
expect(extractBranchName(mockSoftServeHeaders, goodWebhook)).toBe(branch);
});
it("should gracefully handle invalid webhook", () => {
expect(getProviderByHeader({})).toBeNull();
expect(extractCommitMessage(mockSoftServeHeaders, {})).toBe("NEW COMMIT");
expect(extractHash(mockSoftServeHeaders, {})).toBe("NEW COMMIT");
expect(extractBranchName(mockSoftServeHeaders, {})).toBeNull();
});
});

View File

@@ -6,6 +6,7 @@ import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const OUTPUT_BASE = "./__test__/drop/zips/output";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal();
@@ -13,7 +14,10 @@ vi.mock("@dokploy/server/constants", async (importOriginal) => {
// @ts-ignore
...actual,
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
// @ts-ignore
...actual.paths(),
BASE_PATH: OUTPUT_BASE,
APPLICATIONS_PATH: OUTPUT_BASE,
}),
};
});
@@ -29,6 +33,7 @@ const baseApp: ApplicationNested = {
applicationId: "",
previewLabels: [],
createEnvFile: true,
bitbucketRepositorySlug: "",
herokuVersion: "",
giteaBranch: "",
buildServerId: "",
@@ -146,8 +151,179 @@ const baseApp: ApplicationNested = {
dockerContextPath: null,
rollbackActive: false,
stopGracePeriodSwarm: null,
ulimitsSwarm: null,
};
/**
* GHSA-66v7-g3fh-47h3: Remote Code Execution through Path Traversal.
* Validates the exact PoC: ZIP with path traversal entry ../../../../../etc/cron.d/malicious-cron
* plus cover files (package.json, index.js). unzipDrop must reject and never write outside output.
*/
describe("GHSA-66v7-g3fh-47h3 path traversal RCE", () => {
beforeAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
afterAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
it("rejects PoC ZIP: traversal ../../../../../etc/cron.d/malicious-cron + package.json + index.js", async () => {
baseApp.appName = "ghsa-rce";
// PoC payload: same entry name as advisory (Python zipfile keeps it; AdmZip normalizes on add → use placeholder + replace)
const traversalEntry = "../../../../../etc/cron.d/malicious-cron";
const cronPayload = "* * * * * root id\n";
const placeholder = "x".repeat(traversalEntry.length);
const zip = new AdmZip();
zip.addFile(
"package.json",
Buffer.from('{"name": "app", "version": "1.0.0"}'),
);
zip.addFile("index.js", Buffer.from('console.log("Application");'));
zip.addFile(placeholder, Buffer.from(cronPayload));
let buf = Buffer.from(zip.toBuffer());
buf = Buffer.from(
buf.toString("binary").split(placeholder).join(traversalEntry),
"binary",
);
const file = new File([buf as unknown as ArrayBuffer], "exploit.zip");
await expect(unzipDrop(file, baseApp)).rejects.toThrow(
/Path traversal detected.*resolved path escapes output directory/,
);
});
});
describe("security: existing symlink escape", () => {
beforeAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
afterAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
it("should NOT write outside base when directory is a symlink", async () => {
const appName = "symlink-existing";
const output = path.join(APPLICATIONS_PATH, appName, "code");
await fs.mkdir(output, { recursive: true });
// outside target (attacker wants to write here)
const outside = path.join(APPLICATIONS_PATH, "..", "outside");
await fs.mkdir(outside, { recursive: true });
// attacker-controlled symlink inside project
await fs.symlink(outside, path.join(output, "logs"));
// zip looks totally harmless
const zip = new AdmZip();
zip.addFile("logs/pwned.txt", Buffer.from("owned"));
const file = new File([zip.toBuffer() as any], "exploit.zip");
await unzipDrop(file, { ...baseApp, appName });
// if vulnerable -> file exists outside sandbox
const escaped = await fs
.readFile(path.join(outside, "pwned.txt"), "utf8")
.then(() => true)
.catch(() => false);
expect(escaped).toBe(false);
});
});
describe("security: zip symlink entry blocked", () => {
beforeAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
afterAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
it("rejects zip containing real symlink entry", async () => {
const appName = "zip-symlink";
const zipBuffer = await fs.readFile(
path.join(__dirname, "./zips/payload/symlink-entry.zip"),
);
const file = new File([zipBuffer as any], "exploit.zip");
await expect(unzipDrop(file, { ...baseApp, appName })).rejects.toThrow(
/Dangerous node entries are not allowed/,
);
});
});
describe("unzipDrop path under output (no traversal)", () => {
beforeAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
afterAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
it("allows entry etc/cron.d/malicious-cron when under output (no path traversal)", async () => {
baseApp.appName = "cron-under-output";
const zip = new AdmZip();
zip.addFile(
"etc/cron.d/malicious-cron",
Buffer.from("* * * * * root id\n"),
);
zip.addFile("package.json", Buffer.from('{"name":"app"}'));
const file = new File(
[zip.toBuffer() as unknown as ArrayBuffer],
"app.zip",
);
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
await unzipDrop(file, baseApp);
const content = await fs.readFile(
path.join(outputPath, "etc/cron.d/malicious-cron"),
"utf8",
);
expect(content).toBe("* * * * * root id\n");
});
});
describe("security: traversal inside BASE_PATH (sandbox escape)", () => {
beforeAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
afterAll(async () => {
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
});
it("should NOT allow writing outside application directory but inside BASE_PATH", async () => {
const appName = "sandbox-escape";
const base = APPLICATIONS_PATH.replace("/applications", "");
const output = path.join(APPLICATIONS_PATH, appName, "code");
await fs.mkdir(output, { recursive: true });
// attacker writes into traefik config inside base
const zip = new AdmZip();
zip.addFile(
"../../../traefik/dynamic/evil.yml",
Buffer.from("pwned: true"),
);
const file = new File([zip.toBuffer() as any], "exploit.zip");
await unzipDrop(file, { ...baseApp, appName });
const escapedPath = path.join(base, "traefik/dynamic/evil.yml");
const exists = await fs
.readFile(escapedPath)
.then(() => true)
.catch(() => false);
expect(exists).toBe(false);
});
});
describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths();
beforeAll(async () => {
@@ -164,14 +340,12 @@ describe("unzipDrop using real zip files", () => {
try {
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
console.log(`Output Path: ${outputPath}`);
const zipBuffer = zip.toBuffer() as Buffer<ArrayBuffer>;
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "test.txt")).toBe(true);
} catch (err) {
console.log(err);
} finally {
}
});

View File

@@ -0,0 +1 @@
/etc/passwd

View File

@@ -6,6 +6,7 @@ type MockCreateServiceOptions = {
TaskTemplate?: {
ContainerSpec?: {
StopGracePeriod?: number;
Ulimits?: Array<{ Name: string; Soft: number; Hard: number }>;
};
};
[key: string]: unknown;
@@ -13,11 +14,11 @@ type MockCreateServiceOptions = {
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
vi.hoisted(() => {
const inspect = vi.fn<[], Promise<never>>();
const inspect = vi.fn<() => Promise<never>>();
const getService = vi.fn(() => ({ inspect }));
const createService = vi.fn<[MockCreateServiceOptions], Promise<void>>(
async () => undefined,
);
const createService = vi.fn<
(opts: MockCreateServiceOptions) => Promise<void>
>(async () => undefined);
const getRemoteDocker = vi.fn(async () => ({
getService,
createService,
@@ -57,6 +58,7 @@ const createApplication = (
},
replicas: 1,
stopGracePeriodSwarm: 0n,
ulimitsSwarm: null,
serverId: "server-id",
...overrides,
}) as unknown as ApplicationNested;
@@ -80,7 +82,9 @@ describe("mechanizeDockerContainer", () => {
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
const call = createServiceMock.mock.calls[0] as
| [MockCreateServiceOptions]
| undefined;
if (!call) {
throw new Error("createServiceMock should have been called once");
}
@@ -97,7 +101,9 @@ describe("mechanizeDockerContainer", () => {
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
const call = createServiceMock.mock.calls[0] as
| [MockCreateServiceOptions]
| undefined;
if (!call) {
throw new Error("createServiceMock should have been called once");
}
@@ -106,4 +112,50 @@ describe("mechanizeDockerContainer", () => {
"StopGracePeriod",
);
});
it("passes ulimits to ContainerSpec when ulimitsSwarm is defined", async () => {
const ulimits = [
{ Name: "nofile", Soft: 10000, Hard: 20000 },
{ Name: "nproc", Soft: 4096, Hard: 8192 },
];
const application = createApplication({ ulimitsSwarm: ulimits });
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.TaskTemplate?.ContainerSpec?.Ulimits).toEqual(ulimits);
});
it("omits Ulimits when ulimitsSwarm is null", async () => {
const application = createApplication({ ulimitsSwarm: 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.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
});
it("omits Ulimits when ulimitsSwarm is an empty array", async () => {
const application = createApplication({ ulimitsSwarm: [] });
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.TaskTemplate?.ContainerSpec).not.toHaveProperty("Ulimits");
});
});

View File

@@ -0,0 +1,43 @@
import { vi } from "vitest";
/**
* Mock the DB module so tests that import from @dokploy/server (barrel)
* never open a real TCP connection to PostgreSQL (e.g. in CI where no DB runs).
* Without this, loading the server barrel pulls in lib/auth and db, which
* connect to localhost:5432 and cause ECONNREFUSED.
*/
vi.mock("@dokploy/server/db", () => {
const chain = () => chain;
chain.set = () => chain;
chain.where = () => chain;
chain.values = () => chain;
chain.returning = () => Promise.resolve([{}]);
chain.from = () => chain;
chain.innerJoin = () => chain;
chain.then = (resolve: (value: unknown) => void) => {
resolve([]);
};
const tableMock = {
findFirst: vi.fn(() => Promise.resolve(undefined)),
findMany: vi.fn(() => Promise.resolve([])),
insert: vi.fn(() => Promise.resolve([{}])),
update: vi.fn(() => chain),
delete: vi.fn(() => chain),
};
return {
db: {
select: vi.fn(() => chain),
insert: vi.fn(() => ({
values: () => ({ returning: () => Promise.resolve([{}]) }),
})),
update: vi.fn(() => chain),
delete: vi.fn(() => chain),
query: new Proxy({} as Record<string, typeof tableMock>, {
get: () => tableMock,
}),
},
dbUrl: "postgres://mock:mock@localhost:5432/mock",
};
});

View File

@@ -8,6 +8,7 @@ const baseApp: ApplicationNested = {
applicationId: "",
previewLabels: [],
createEnvFile: true,
bitbucketRepositorySlug: "",
herokuVersion: "",
giteaRepository: "",
giteaOwner: "",
@@ -124,6 +125,7 @@ const baseApp: ApplicationNested = {
username: null,
dockerContextPath: null,
stopGracePeriodSwarm: null,
ulimitsSwarm: null,
};
const baseDomain: Domain = {
@@ -273,3 +275,51 @@ test("CertificateType on websecure entrypoint", async () => {
expect(router.tls?.certResolver).toBe("letsencrypt");
});
/** IDN/Punycode */
test("Internationalized domain name is converted to punycode", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, host: "тест.рф" },
"web",
);
// тест.рф in punycode is xn--e1aybc.xn--p1ai
expect(router.rule).toContain("Host(`xn--e1aybc.xn--p1ai`)");
expect(router.rule).not.toContain("тест.рф");
});
test("ASCII domain remains unchanged", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, host: "example.com" },
"web",
);
expect(router.rule).toContain("Host(`example.com`)");
});
test("Russian Cyrillic label with .ru TLD is converted to punycode", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, host: "сайт.ru" },
"web",
);
// сайт in punycode is xn--80aswg
expect(router.rule).toContain("Host(`xn--80aswg.ru`)");
expect(router.rule).not.toContain("сайт");
});
test("Subdomain with Russian IDN TLD converts non-ASCII part to punycode", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, host: "app.тест.рф" },
"web",
);
// app stays ASCII, тест.рф becomes xn--e1aybc.xn--p1ai
expect(router.rule).toContain("Host(`app.xn--e1aybc.xn--p1ai`)");
expect(router.rule).not.toContain("тест.рф");
});

View File

@@ -7,10 +7,15 @@ export default defineConfig({
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
pool: "forks",
setupFiles: [path.resolve(__dirname, "setup.ts")],
},
define: {
"process.env": {
NODE: "test",
GITHUB_CLIENT_ID: "test",
GITHUB_CLIENT_SECRET: "test",
GOOGLE_CLIENT_ID: "test",
GOOGLE_CLIENT_SECRET: "test",
},
},
plugins: [

View File

@@ -0,0 +1,81 @@
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
const BASE = "/base";
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@dokploy/server/constants")>();
return {
...actual,
paths: () => ({
...actual.paths(),
BASE_PATH: BASE,
LOGS_PATH: `${BASE}/logs`,
APPLICATIONS_PATH: `${BASE}/applications`,
}),
};
});
// Import after mock so paths() uses our BASE
const { readValidDirectory } = await import("@dokploy/server");
describe("readValidDirectory (path traversal)", () => {
it("returns true when directory is exactly BASE_PATH", () => {
expect(readValidDirectory(BASE)).toBe(true);
expect(readValidDirectory(path.resolve(BASE))).toBe(true);
});
it("returns true when directory is under BASE_PATH", () => {
expect(readValidDirectory(`${BASE}/logs`)).toBe(true);
expect(readValidDirectory(`${BASE}/logs/app/foo.log`)).toBe(true);
expect(readValidDirectory(`${BASE}/applications/myapp/code`)).toBe(true);
});
it("returns false for path traversal escaping base (absolute)", () => {
expect(readValidDirectory("/etc/passwd")).toBe(false);
expect(readValidDirectory("/etc/cron.d/malicious")).toBe(false);
expect(readValidDirectory("/tmp/outside")).toBe(false);
});
it("returns false when resolved path escapes base via ..", () => {
// Resolved: /etc/passwd (outside /base)
expect(readValidDirectory(`${BASE}/../etc/passwd`)).toBe(false);
expect(readValidDirectory(`${BASE}/logs/../../etc/passwd`)).toBe(false);
expect(readValidDirectory(`${BASE}/..`)).toBe(false);
});
it("returns true when .. stays within base", () => {
// e.g. /base/logs/../applications -> /base/applications (still under /base)
expect(readValidDirectory(`${BASE}/logs/../applications`)).toBe(true);
expect(readValidDirectory(`${BASE}/foo/../bar`)).toBe(true);
});
it("accepts serverId for remote base path", () => {
// With our mock, serverId doesn't change BASE_PATH; just ensure it doesn't throw
expect(readValidDirectory(BASE, "server-1")).toBe(true);
expect(readValidDirectory("/etc/passwd", "server-1")).toBe(false);
});
it("returns false for null/undefined-like paths that resolve outside", () => {
// Paths that might resolve to cwd or root
expect(readValidDirectory(".")).toBe(false);
expect(readValidDirectory("..")).toBe(false);
});
it("returns true for BASE_PATH with trailing slash or double slashes under base", () => {
expect(readValidDirectory(`${BASE}/`)).toBe(true);
expect(readValidDirectory(`${BASE}//logs`)).toBe(true);
expect(readValidDirectory(`${BASE}/applications///myapp/code`)).toBe(true);
});
it("returns false when path looks like base but is a sibling or prefix", () => {
expect(readValidDirectory("/base-evil")).toBe(false);
expect(readValidDirectory("/bas")).toBe(false);
expect(readValidDirectory(`${BASE}/../base-evil`)).toBe(false);
});
it("returns false for empty string (resolves to cwd)", () => {
expect(readValidDirectory("")).toBe(false);
});
});

View File

@@ -0,0 +1,132 @@
import { describe, expect, it } from "vitest";
import {
isValidContainerId,
isValidSearch,
isValidSince,
isValidTail,
} from "../../server/wss/utils";
describe("isValidTail (docker-container-logs)", () => {
it("accepts valid numeric tail values", () => {
expect(isValidTail("0")).toBe(true);
expect(isValidTail("1")).toBe(true);
expect(isValidTail("100")).toBe(true);
expect(isValidTail("10000")).toBe(true);
});
it("rejects tail above 10000", () => {
expect(isValidTail("10001")).toBe(false);
expect(isValidTail("99999")).toBe(false);
});
it("rejects non-numeric tail", () => {
expect(isValidTail("")).toBe(false);
expect(isValidTail("abc")).toBe(false);
expect(isValidTail("10a")).toBe(false);
expect(isValidTail("-1")).toBe(false);
});
it("rejects command injection payloads in tail", () => {
expect(isValidTail("10; whoami; #")).toBe(false);
expect(isValidTail("100 | cat /etc/passwd")).toBe(false);
expect(isValidTail("$(id)")).toBe(false);
expect(isValidTail("`id`")).toBe(false);
expect(isValidTail("100\nid")).toBe(false);
expect(isValidTail("100 && id")).toBe(false);
expect(isValidTail("100; env | grep DATABASE")).toBe(false);
});
});
describe("isValidSince (docker-container-logs)", () => {
it("accepts 'all'", () => {
expect(isValidSince("all")).toBe(true);
});
it("accepts valid duration format (number + s|m|h|d)", () => {
expect(isValidSince("5s")).toBe(true);
expect(isValidSince("10m")).toBe(true);
expect(isValidSince("1h")).toBe(true);
expect(isValidSince("2d")).toBe(true);
expect(isValidSince("0s")).toBe(true);
expect(isValidSince("999d")).toBe(true);
});
it("rejects invalid duration format", () => {
expect(isValidSince("")).toBe(false);
expect(isValidSince("5")).toBe(false);
expect(isValidSince("s")).toBe(false);
expect(isValidSince("5x")).toBe(false);
expect(isValidSince("5sec")).toBe(false);
expect(isValidSince("5 m")).toBe(false);
});
it("rejects command injection payloads in since", () => {
expect(isValidSince("5s; whoami")).toBe(false);
expect(isValidSince("all; id")).toBe(false);
expect(isValidSince("1m$(id)")).toBe(false);
expect(isValidSince("1m | cat /etc/passwd")).toBe(false);
});
});
describe("isValidSearch (docker-container-logs)", () => {
it("accepts empty string", () => {
expect(isValidSearch("")).toBe(true);
});
it("accepts only alphanumeric, space, dot, underscore, hyphen", () => {
expect(isValidSearch("error")).toBe(true);
expect(isValidSearch("foo bar")).toBe(true);
expect(isValidSearch("a-zA-Z0-9_.-")).toBe(true);
expect(isValidSearch("")).toBe(true);
});
it("rejects strings longer than 500 chars", () => {
expect(isValidSearch("a".repeat(501))).toBe(false);
expect(isValidSearch("a".repeat(500))).toBe(true);
});
it("rejects control characters and non-printable", () => {
expect(isValidSearch("foo\nbar")).toBe(false);
expect(isValidSearch("foo\rbar")).toBe(false);
expect(isValidSearch("\x00")).toBe(false);
expect(isValidSearch("a\x19b")).toBe(false);
});
it("rejects command injection vectors in search (search is concatenated into shell)", () => {
// Double-quoted context (SSH line 99): $ and ` execute
expect(isValidSearch("$(whoami)")).toBe(false);
expect(isValidSearch("`id`")).toBe(false);
expect(isValidSearch("$(id)")).toBe(false);
// Single-quoted context (local line 153): ' breaks out
expect(isValidSearch("'$(whoami)'")).toBe(false);
expect(isValidSearch("error'")).toBe(false);
expect(isValidSearch("'; whoami; #")).toBe(false);
// Other shell-metacharacters
expect(isValidSearch("error; id")).toBe(false);
expect(isValidSearch("a|b")).toBe(false);
expect(isValidSearch('error"')).toBe(false);
expect(isValidSearch("a&b")).toBe(false);
});
});
describe("isValidContainerId (docker-container-logs)", () => {
it("accepts valid hex container IDs", () => {
expect(isValidContainerId("a".repeat(12))).toBe(true);
expect(isValidContainerId("abc123def456")).toBe(true);
expect(isValidContainerId("a".repeat(64))).toBe(true);
});
it("accepts valid container names", () => {
expect(isValidContainerId("my-container")).toBe(true);
expect(isValidContainerId("app_1")).toBe(true);
expect(isValidContainerId("service.name")).toBe(true);
});
it("rejects command injection in container ID", () => {
expect(isValidContainerId("dummy; whoami")).toBe(false);
expect(isValidContainerId("$(id)")).toBe(false);
expect(isValidContainerId("`id`")).toBe(false);
expect(isValidContainerId("container|cat /etc/passwd")).toBe(false);
expect(isValidContainerId("x; env | grep DATABASE")).toBe(false);
});
});

View File

@@ -22,6 +22,7 @@ import {
HealthCheckForm,
LabelsForm,
ModeForm,
NetworkForm,
PlacementForm,
RestartPolicyForm,
RollbackConfigForm,
@@ -79,6 +80,13 @@ const menuItems: MenuItem[] = [
docDescription:
"Set service mode to either 'Replicated' with a specified number of tasks (Replicas), or 'Global' (one task per node).",
},
{
id: "network",
label: "Network",
description: "Configure network attachments",
docDescription:
"Attach the service to one or more networks. Specify the network name (Target) and optional network aliases for service discovery.",
},
{
id: "labels",
label: "Labels",
@@ -190,6 +198,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
<RollbackConfigForm id={id} type={type} />
)}
{activeMenu === "mode" && <ModeForm id={id} type={type} />}
{activeMenu === "network" && <NetworkForm id={id} type={type} />}
{activeMenu === "labels" && <LabelsForm id={id} type={type} />}
{activeMenu === "stop-grace-period" && (
<StopGracePeriodForm id={id} type={type} />

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -73,7 +73,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
@@ -236,7 +236,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
)}
<div className="flex justify-end">
<Button isLoading={isLoading} type="submit" className="w-fit">
<Button isLoading={isPending} type="submit" className="w-fit">
Save
</Button>
</div>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -31,7 +31,6 @@ interface HealthCheckFormProps {
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
const [isLoading, setIsLoading] = useState(false);
const [testCommands, setTestCommands] = useState<string[]>([]);
const queryMap = {
postgres: () =>
@@ -72,6 +71,8 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
},
});
const testCommands = form.watch("Test") || [];
useEffect(() => {
if (data?.healthCheckSwarm) {
const hc = data.healthCheckSwarm;
@@ -82,7 +83,6 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
StartPeriod: hc.StartPeriod,
Retries: hc.Retries,
});
setTestCommands(hc.Test || []);
}
}, [data, form]);
@@ -117,17 +117,20 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
};
const addTestCommand = () => {
setTestCommands([...testCommands, ""]);
form.setValue("Test", [...testCommands, ""]);
};
const updateTestCommand = (index: number, value: string) => {
const newCommands = [...testCommands];
newCommands[index] = value;
setTestCommands(newCommands);
form.setValue("Test", newCommands);
};
const removeTestCommand = (index: number) => {
setTestCommands(testCommands.filter((_, i) => i !== index));
form.setValue(
"Test",
testCommands.filter((_: string, i: number) => i !== index),
);
};
return (
@@ -140,7 +143,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
http://localhost:3000/health"])
</FormDescription>
<div className="space-y-2 mt-2">
{testCommands.map((cmd, index) => (
{testCommands.map((cmd: string, index: number) => (
<div key={index} className="flex gap-2">
<Input
value={cmd}

View File

@@ -1,10 +1,11 @@
export { HealthCheckForm } from "./health-check-form";
export { RestartPolicyForm } from "./restart-policy-form";
export { PlacementForm } from "./placement-form";
export { UpdateConfigForm } from "./update-config-form";
export { RollbackConfigForm } from "./rollback-config-form";
export { ModeForm } from "./mode-form";
export { LabelsForm } from "./labels-form";
export { StopGracePeriodForm } from "./stop-grace-period-form";
export { EndpointSpecForm } from "./endpoint-spec-form";
export { HealthCheckForm } from "./health-check-form";
export { LabelsForm } from "./labels-form";
export { ModeForm } from "./mode-form";
export { NetworkForm } from "./network-form";
export { PlacementForm } from "./placement-form";
export { RestartPolicyForm } from "./restart-policy-form";
export { RollbackConfigForm } from "./rollback-config-form";
export { StopGracePeriodForm } from "./stop-grace-period-form";
export { UpdateConfigForm } from "./update-config-form";
export { filterEmptyValues, hasValues } from "./utils";

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -105,7 +105,14 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
const modeData =
formData.type === "Replicated"
? { Replicated: { Replicas: formData.Replicas } }
? {
Replicated: {
Replicas:
formData.Replicas !== undefined && formData.Replicas !== ""
? Number(formData.Replicas)
: undefined,
},
}
: { Global: {} };
await mutateAsync({

View File

@@ -0,0 +1,313 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
const driverOptEntrySchema = z.object({
key: z.string(),
value: z.string(),
});
export const networkFormSchema = z.object({
networks: z
.array(
z.object({
Target: z.string().optional(),
Aliases: z.string().optional(),
DriverOptsEntries: z.array(driverOptEntrySchema).optional(),
}),
)
.optional(),
});
interface NetworkFormProps {
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
}
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
const [isLoading, setIsLoading] = useState(false);
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<z.infer<typeof networkFormSchema>>({
resolver: zodResolver(networkFormSchema),
defaultValues: {
networks: [],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "networks",
});
useEffect(() => {
if (data?.networkSwarm && Array.isArray(data.networkSwarm)) {
const networkEntries = data.networkSwarm.map((network) => ({
Target: network.Target || "",
Aliases: network.Aliases?.join(", ") || "",
DriverOptsEntries: network.DriverOpts
? Object.entries(network.DriverOpts).map(([key, value]) => ({
key,
value: value ?? "",
}))
: [],
}));
form.reset({ networks: networkEntries });
}
}, [data, form]);
const onSubmit = async (formData: z.infer<typeof networkFormSchema>) => {
setIsLoading(true);
try {
const networksArray =
formData.networks
?.filter((network) => network.Target)
.map((network) => {
const entries = (network.DriverOptsEntries ?? []).filter(
(e) => e.key.trim() !== "",
);
const driverOpts =
entries.length > 0
? Object.fromEntries(
entries.map((e) => [e.key.trim(), e.value]),
)
: undefined;
return {
Target: network.Target,
Aliases: network.Aliases
? network.Aliases.split(",").map((alias) => alias.trim())
: undefined,
DriverOpts: driverOpts,
};
}) || [];
// If no networks, send null to clear the database
const networksToSend = networksArray.length > 0 ? networksArray : null;
await mutateAsync({
applicationId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
networkSwarm: networksToSend,
});
toast.success("Network configuration updated successfully");
refetch();
} catch {
toast.error("Error updating network configuration");
} finally {
setIsLoading(false);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div>
<FormLabel>Networks</FormLabel>
<FormDescription>
Configure network attachments for your service
</FormDescription>
<div className="space-y-2 mt-2">
{fields.map((field, index) => (
<div key={field.id} className="space-y-2 p-3 border rounded">
<FormField
control={form.control}
name={`networks.${index}.Target`}
render={({ field }) => (
<FormItem>
<FormLabel>Network Name</FormLabel>
<FormControl>
<Input {...field} placeholder="my-network" />
</FormControl>
<FormDescription>
The name of the network to attach to
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`networks.${index}.Aliases`}
render={({ field }) => (
<FormItem>
<FormLabel>Aliases (optional)</FormLabel>
<FormControl>
<Input
{...field}
placeholder="alias1, alias2, alias3"
/>
</FormControl>
<FormDescription>
Comma-separated list of network aliases
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-2">
<FormLabel>Driver options (optional)</FormLabel>
<FormDescription>
e.g. com.docker.network.driver.mtu,
com.docker.network.driver.host_binding
</FormDescription>
{(
form.watch(`networks.${index}.DriverOptsEntries`) ?? []
).map((_, optIndex) => (
<div
key={optIndex}
className="flex gap-2 items-end flex-wrap"
>
<FormField
control={form.control}
name={`networks.${index}.DriverOptsEntries.${optIndex}.key`}
render={({ field }) => (
<FormItem className="flex-1 min-w-[140px]">
<FormControl>
<Input
{...field}
placeholder="com.docker.network.driver.mtu"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`networks.${index}.DriverOptsEntries.${optIndex}.value`}
render={({ field }) => (
<FormItem className="flex-1 min-w-[100px]">
<FormControl>
<Input {...field} placeholder="1500" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
const entries =
form.getValues(
`networks.${index}.DriverOptsEntries`,
) ?? [];
form.setValue(
`networks.${index}.DriverOptsEntries`,
entries.filter((_, i) => i !== optIndex),
);
}}
>
Remove
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const entries =
form.getValues(`networks.${index}.DriverOptsEntries`) ??
[];
form.setValue(`networks.${index}.DriverOptsEntries`, [
...entries,
{ key: "", value: "" },
]);
}}
>
Add driver option
</Button>
</div>
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => remove(index)}
>
Remove Network
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
append({
Target: "",
Aliases: "",
DriverOptsEntries: [],
})
}
>
Add Network
</Button>
</div>
</div>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => {
form.reset({ networks: [] });
}}
>
Clear
</Button>
<Button type="submit" isLoading={isLoading}>
Save Networks
</Button>
</div>
</form>
</Form>
);
};

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -17,9 +17,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
const PreferenceSchema = z.object({
Spread: z.object({
SpreadDescriptor: z.string(),
}),
SpreadDescriptor: z.string(),
});
const PlatformSchema = z.object({
@@ -116,7 +114,14 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
placementSwarm: hasAnyValue ? formData : null,
placementSwarm: hasAnyValue
? {
...formData,
Preferences: formData.Preferences?.map((p) => ({
Spread: { SpreadDescriptor: p.SpreadDescriptor },
})),
}
: null,
});
toast.success("Placement updated successfully");

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
@@ -50,7 +50,7 @@ export const AddCommand = ({ applicationId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isLoading } = api.application.update.useMutation();
const { mutateAsync, isPending } = api.application.update.useMutation();
const form = useForm<AddCommand>({
defaultValues: {
@@ -177,7 +177,7 @@ export const AddCommand = ({ applicationId }: Props) => {
</div>
</div>
<div className="flex justify-end">
<Button isLoading={isLoading} type="submit" className="w-fit">
<Button isLoading={isPending} type="submit" className="w-fit">
Save
</Button>
</div>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Code2, Globe2, HardDrive } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -69,11 +69,11 @@ export const ShowImport = ({ composeId }: Props) => {
} | null>(null);
const utils = api.useUtils();
const { mutateAsync: processTemplate, isLoading: isLoadingTemplate } =
const { mutateAsync: processTemplate, isPending: isLoadingTemplate } =
api.compose.processTemplate.useMutation();
const {
mutateAsync: importTemplate,
isLoading: isImporting,
isPending: isImporting,
isSuccess: isImportSuccess,
} = api.compose.import.useMutation();

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
@@ -35,13 +35,9 @@ import { api } from "@/utils/api";
const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535),
publishMode: z.enum(["ingress", "host"], {
required_error: "Publish mode is required",
}),
publishMode: z.enum(["ingress", "host"]),
targetPort: z.number().int().min(1).max(65535),
protocol: z.enum(["tcp", "udp"], {
required_error: "Protocol is required",
}),
protocol: z.enum(["tcp", "udp"]),
});
type AddPort = z.infer<typeof AddPortSchema>;
@@ -68,7 +64,7 @@ export const HandlePorts = ({
enabled: !!portId,
},
);
const { mutateAsync, isLoading, error, isError } = portId
const { mutateAsync, isPending, error, isError } = portId
? api.port.update.useMutation()
: api.port.create.useMutation();
@@ -270,7 +266,7 @@ export const HandlePorts = ({
<DialogFooter>
<Button
isLoading={isLoading}
isLoading={isPending}
form="hook-form-add-port"
type="submit"
>

View File

@@ -25,7 +25,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync: deletePort, isLoading: isRemoving } =
const { mutateAsync: deletePort, isPending: isRemoving } =
api.port.delete.useMutation();
return (

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -100,11 +100,11 @@ export const HandleRedirect = ({
const utils = api.useUtils();
const { mutateAsync, isLoading, error, isError } = redirectId
const { mutateAsync, isPending, error, isError } = redirectId
? api.redirects.update.useMutation()
: api.redirects.create.useMutation();
const form = useForm<AddRedirect>({
const form = useForm({
defaultValues: {
permanent: false,
regex: "",
@@ -268,7 +268,7 @@ export const HandleRedirect = ({
<DialogFooter>
<Button
isLoading={isLoading}
isLoading={isPending}
form="hook-form-add-redirect"
type="submit"
>

View File

@@ -24,7 +24,7 @@ export const ShowRedirects = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
const { mutateAsync: deleteRedirect, isPending: isRemoving } =
api.redirects.delete.useMutation();
const utils = api.useUtils();

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -46,7 +46,7 @@ export const HandleSecurity = ({
}: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data } = api.security.one.useQuery(
const { data, refetch } = api.security.one.useQuery(
{
securityId: securityId ?? "",
},
@@ -55,7 +55,7 @@ export const HandleSecurity = ({
},
);
const { mutateAsync, isLoading, error, isError } = securityId
const { mutateAsync, isPending, error, isError } = securityId
? api.security.update.useMutation()
: api.security.create.useMutation();
@@ -88,6 +88,7 @@ export const HandleSecurity = ({
await utils.application.readTraefikConfig.invalidate({
applicationId,
});
await refetch();
setIsOpen(false);
})
.catch(() => {
@@ -163,7 +164,7 @@ export const HandleSecurity = ({
<DialogFooter>
<Button
isLoading={isLoading}
isLoading={isPending}
form="hook-form-add-security"
type="submit"
>

View File

@@ -27,7 +27,7 @@ export const ShowSecurity = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
const { mutateAsync: deleteSecurity, isPending: isRemoving } =
api.security.delete.useMutation();
const utils = api.useUtils();

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -74,7 +74,7 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
const { data: buildServers } = api.server.buildServers.useQuery();
const { data: registries } = api.registry.all.useQuery();
const { mutateAsync, isLoading } = api.application.update.useMutation();
const { mutateAsync, isPending } = api.application.update.useMutation();
const form = useForm<Schema>({
defaultValues: {
@@ -274,7 +274,7 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
/>
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
<Button isLoading={isPending} type="submit">
Save
</Button>
</div>

View File

@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { InfoIcon, Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
@@ -21,10 +21,18 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
createConverter,
NumberInputWithSteps,
} from "@/components/ui/number-input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
@@ -50,13 +58,36 @@ const memoryConverter = createConverter(1024 * 1024, (mb) => {
: `${formatNumber(mb)} MB`;
});
const ulimitSchema = z.object({
Name: z.string().min(1, "Name is required"),
Soft: z.coerce.number().int().min(-1, "Must be >= -1"),
Hard: z.coerce.number().int().min(-1, "Must be >= -1"),
});
const addResourcesSchema = z.object({
memoryReservation: z.string().optional(),
cpuLimit: z.string().optional(),
memoryLimit: z.string().optional(),
cpuReservation: z.string().optional(),
ulimitsSwarm: z.array(ulimitSchema).optional(),
});
const ULIMIT_PRESETS = [
{ value: "nofile", label: "nofile (Open Files)" },
{ value: "nproc", label: "nproc (Processes)" },
{ value: "memlock", label: "memlock (Locked Memory)" },
{ value: "stack", label: "stack (Stack Size)" },
{ value: "core", label: "core (Core File Size)" },
{ value: "cpu", label: "cpu (CPU Time)" },
{ value: "data", label: "data (Data Segment)" },
{ value: "fsize", label: "fsize (File Size)" },
{ value: "locks", label: "locks (File Locks)" },
{ value: "msgqueue", label: "msgqueue (Message Queues)" },
{ value: "nice", label: "nice (Nice Priority)" },
{ value: "rtprio", label: "rtprio (Real-time Priority)" },
{ value: "sigpending", label: "sigpending (Pending Signals)" },
];
export type ServiceType =
| "postgres"
| "mongo"
@@ -97,20 +128,26 @@ export const ShowResources = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddResources>({
const form = useForm({
defaultValues: {
cpuLimit: "",
cpuReservation: "",
memoryLimit: "",
memoryReservation: "",
ulimitsSwarm: [],
},
resolver: zodResolver(addResourcesSchema),
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "ulimitsSwarm",
});
useEffect(() => {
if (data) {
form.reset({
@@ -118,6 +155,7 @@ export const ShowResources = ({ id, type }: Props) => {
cpuReservation: data?.cpuReservation || undefined,
memoryLimit: data?.memoryLimit || undefined,
memoryReservation: data?.memoryReservation || undefined,
ulimitsSwarm: data?.ulimitsSwarm || [],
});
}
}, [data, form, form.reset]);
@@ -134,6 +172,10 @@ export const ShowResources = ({ id, type }: Props) => {
cpuReservation: formData.cpuReservation || null,
memoryLimit: formData.memoryLimit || null,
memoryReservation: formData.memoryReservation || null,
ulimitsSwarm:
formData.ulimitsSwarm && formData.ulimitsSwarm.length > 0
? formData.ulimitsSwarm
: null,
})
.then(async () => {
toast.success("Resources Updated");
@@ -325,8 +367,157 @@ export const ShowResources = ({ id, type }: Props) => {
}}
/>
</div>
{/* Ulimits Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FormLabel className="text-base">Ulimits</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>
Set resource limits for the container. Each ulimit has
a soft limit (warning threshold) and hard limit
(maximum allowed). Use -1 for unlimited.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
append({ Name: "nofile", Soft: 65535, Hard: 65535 })
}
>
<Plus className="h-4 w-4 mr-1" />
Add Ulimit
</Button>
</div>
{fields.length > 0 && (
<div className="space-y-3">
{fields.map((field, index) => (
<div
key={field.id}
className="flex items-start gap-3 p-3 border rounded-lg bg-muted/30"
>
<FormField
control={form.control}
name={`ulimitsSwarm.${index}.Name`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel className="text-xs">Type</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select ulimit" />
</SelectTrigger>
</FormControl>
<SelectContent>
{ULIMIT_PRESETS.map((preset) => (
<SelectItem
key={preset.value}
value={preset.value}
>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ulimitsSwarm.${index}.Soft`}
render={({ field }) => (
<FormItem className="w-32">
<FormLabel className="text-xs">
Soft Limit
</FormLabel>
<FormControl>
<Input
type="number"
min={-1}
placeholder="65535"
{...field}
value={
typeof field.value === "number"
? field.value
: ""
}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ulimitsSwarm.${index}.Hard`}
render={({ field }) => (
<FormItem className="w-32">
<FormLabel className="text-xs">
Hard Limit
</FormLabel>
<FormControl>
<Input
type="number"
min={-1}
placeholder="65535"
{...field}
value={
typeof field.value === "number"
? field.value
: ""
}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="mt-6 text-destructive hover:text-destructive"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
{fields.length === 0 && (
<p className="text-sm text-muted-foreground">
No ulimits configured. Click &quot;Add Ulimit&quot; to set
resource limits.
</p>
)}
</div>
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
<Button isLoading={isPending} type="submit">
Save
</Button>
</div>

View File

@@ -15,7 +15,7 @@ interface Props {
}
export const ShowTraefikConfig = ({ applicationId }: Props) => {
const { data, isLoading } = api.application.readTraefikConfig.useQuery(
const { data, isPending } = api.application.readTraefikConfig.useQuery(
{
applicationId,
},
@@ -35,7 +35,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{isLoading ? (
{isPending ? (
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center justify-center min-h-[10vh]">
Loading...
<Loader2 className="animate-spin" />

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -7,6 +7,7 @@ import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -24,6 +25,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
const UpdateTraefikConfigSchema = z.object({
@@ -59,6 +61,7 @@ export const validateAndFormatYAML = (yamlText: string) => {
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
const [open, setOpen] = useState(false);
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
const { data, refetch } = api.application.readTraefikConfig.useQuery(
{
applicationId,
@@ -66,7 +69,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync, isLoading, error, isError } =
const { mutateAsync, isPending, error, isError } =
api.application.updateTraefikConfig.useMutation();
const form = useForm<UpdateTraefikConfig>({
@@ -85,13 +88,15 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
}, [data]);
const onSubmit = async (data: UpdateTraefikConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: (error as string) || "Invalid YAML",
});
return;
if (!skipYamlValidation) {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: (error as string) || "Invalid YAML",
});
return;
}
}
form.clearErrors("traefikConfig");
await mutateAsync({
@@ -116,11 +121,12 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
setOpen(open);
if (!open) {
form.reset();
setSkipYamlValidation(false);
}
}}
>
<DialogTrigger asChild>
<Button isLoading={isLoading}>Modify</Button>
<Button isLoading={isPending}>Modify</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
@@ -169,9 +175,30 @@ routers:
</div>
</form>
<DialogFooter>
<DialogFooter className="flex-col sm:flex-row gap-4">
<div className="flex flex-col gap-1 w-full sm:w-auto sm:mr-auto">
<div className="flex items-center space-x-2">
<Checkbox
id="skip-yaml-validation-app"
checked={skipYamlValidation}
onCheckedChange={(checked) =>
setSkipYamlValidation(checked === true)
}
/>
<Label
htmlFor="skip-yaml-validation-app"
className="text-sm font-normal cursor-pointer"
>
Skip YAML validation (for Go templating)
</Label>
</div>
<p className="text-sm text-muted-foreground">
Check to save configs with Go templating (e.g.{" "}
<code className="text-xs">{"{{range}}"}</code>).
</p>
</div>
<Button
isLoading={isLoading}
isLoading={isPending}
form="hook-form-update-traefik-config"
type="submit"
>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PlusIcon } from "lucide-react";
import type React from "react";
import { useEffect, useState } from "react";

View File

@@ -37,7 +37,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { mutateAsync: deleteVolume, isLoading: isRemoving } =
const { mutateAsync: deleteVolume, isPending: isRemoving } =
api.mounts.remove.useMutation();
return (
<Card className="bg-background">

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -93,7 +93,7 @@ export const UpdateVolume = ({
},
);
const { mutateAsync, isLoading, error, isError } =
const { mutateAsync, isPending, error, isError } =
api.mounts.update.useMutation();
const form = useForm<UpdateMount>({
@@ -187,7 +187,7 @@ export const UpdateVolume = ({
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
isLoading={isLoading}
isLoading={isPending}
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
@@ -310,7 +310,7 @@ PORT=3000
</div>
<DialogFooter>
<Button
isLoading={isLoading}
isLoading={isPending}
// form="hook-form-update-volume"
type="submit"
>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Cog } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -74,12 +74,7 @@ const buildTypeDisplayMap: Record<BuildType, string> = {
const mySchema = z.discriminatedUnion("buildType", [
z.object({
buildType: z.literal(BuildType.dockerfile),
dockerfile: z
.string({
required_error: "Dockerfile path is required",
invalid_type_error: "Dockerfile path is required",
})
.min(1, "Dockerfile required"),
dockerfile: z.string().nullable().default(""),
dockerContextPath: z.string().nullable().default(""),
dockerBuildStage: z.string().nullable().default(""),
}),
@@ -168,14 +163,14 @@ const resetData = (data: ApplicationData): AddTemplate => {
};
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } =
const { mutateAsync, isPending } =
api.application.saveBuildType.useMutation();
const { data, refetch } = api.application.one.useQuery(
{ applicationId },
{ enabled: !!applicationId },
);
const form = useForm<AddTemplate>({
const form = useForm({
defaultValues: {
buildType: BuildType.nixpacks,
},
@@ -347,7 +342,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
<FormLabel>Docker File</FormLabel>
<FormControl>
<Input
placeholder="Path of your docker file"
placeholder="Path of your docker file (default: Dockerfile)"
{...field}
value={field.value ?? ""}
/>
@@ -533,7 +528,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
</>
)}
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
<Button isLoading={isPending} type="submit">
Save
</Button>
</div>

View File

@@ -1,4 +1,4 @@
import { Paintbrush } from "lucide-react";
import { Ban } from "lucide-react";
import { toast } from "sonner";
import {
AlertDialog,
@@ -20,7 +20,7 @@ interface Props {
}
export const CancelQueues = ({ id, type }: Props) => {
const { mutateAsync, isLoading } =
const { mutateAsync, isPending } =
type === "application"
? api.application.cleanQueues.useMutation()
: api.compose.cleanQueues.useMutation();
@@ -33,9 +33,9 @@ export const CancelQueues = ({ id, type }: Props) => {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
<Button variant="destructive" className="w-fit" isLoading={isPending}>
Cancel Queues
<Paintbrush className="size-4" />
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>

View File

@@ -0,0 +1,73 @@
import { Paintbrush } from "lucide-react";
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
interface Props {
id: string;
type: "application" | "compose";
}
export const ClearDeployments = ({ id, type }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isPending } =
type === "application"
? api.application.clearDeployments.useMutation()
: api.compose.clearDeployments.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="w-fit" isLoading={isPending}>
Clear deployments
<Paintbrush className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to clear old deployments?
</AlertDialogTitle>
<AlertDialogDescription>
This will delete all old deployment records and logs, keeping only
the active deployment (the most recent successful one).
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId: id || "",
composeId: id || "",
})
.then(async () => {
toast.success("Old deployments cleared successfully");
await utils.deployment.allByType.invalidate({
id,
type: type as "application" | "compose",
});
})
.catch((err) => {
toast.error(err.message);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -20,7 +20,7 @@ interface Props {
}
export const KillBuild = ({ id, type }: Props) => {
const { mutateAsync, isLoading } =
const { mutateAsync, isPending } =
type === "application"
? api.application.killBuild.useMutation()
: api.compose.killBuild.useMutation();
@@ -28,7 +28,7 @@ export const KillBuild = ({ id, type }: Props) => {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="w-fit" isLoading={isLoading}>
<Button variant="outline" className="w-fit" isLoading={isPending}>
Kill Build
<Scissors className="size-4" />
</Button>

View File

@@ -194,13 +194,21 @@ export const ShowDeployment = ({
{" "}
{filteredLogs.length > 0 ? (
filteredLogs.map((log: LogLine, index: number) => (
<TerminalLine key={index} log={log} noTimestamp />
<TerminalLine
key={`${log.rawTimestamp ?? ""}-${index}`}
log={log}
noTimestamp
/>
))
) : (
<>
{optionalErrors.length > 0 ? (
optionalErrors.map((log: LogLine, index: number) => (
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
<TerminalLine
key={`extra-${log.rawTimestamp ?? ""}-${index}`}
log={log}
noTimestamp
/>
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">

View File

@@ -6,6 +6,7 @@ import {
RefreshCcw,
RocketIcon,
Settings,
Trash2,
} from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
@@ -25,6 +26,7 @@ import {
import { api, type RouterOutputs } from "@/utils/api";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { CancelQueues } from "./cancel-queues";
import { ClearDeployments } from "./clear-deployments";
import { KillBuild } from "./kill-build";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
@@ -59,7 +61,7 @@ export const ShowDeployments = ({
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const { data: deployments, isLoading: isLoadingDeployments } =
const { data: deployments, isPending: isLoadingDeployments } =
api.deployment.allByType.useQuery(
{
id,
@@ -73,19 +75,21 @@ export const ShowDeployments = ({
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: rollback, isLoading: isRollingBack } =
const { mutateAsync: rollback, isPending: isRollingBack } =
api.rollback.rollback.useMutation();
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
const { mutateAsync: killProcess, isPending: isKillingProcess } =
api.deployment.killProcess.useMutation();
const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
api.deployment.removeDeployment.useMutation();
// Cancel deployment mutations
const {
mutateAsync: cancelApplicationDeployment,
isLoading: isCancellingApp,
isPending: isCancellingApp,
} = api.application.cancelDeployment.useMutation();
const {
mutateAsync: cancelComposeDeployment,
isLoading: isCancellingCompose,
isPending: isCancellingCompose,
} = api.compose.cancelDeployment.useMutation();
const [url, setUrl] = React.useState("");
@@ -144,6 +148,9 @@ export const ShowDeployments = ({
</CardDescription>
</div>
<div className="flex flex-row items-center flex-wrap gap-2">
{(type === "application" || type === "compose") && (
<ClearDeployments id={id} type={type} />
)}
{(type === "application" || type === "compose") && (
<KillBuild id={id} type={type} />
)}
@@ -252,6 +259,8 @@ export const ShowDeployments = ({
const isExpanded = expandedDescriptions.has(
deployment.deploymentId,
);
const canDelete =
deployment.status === "done" || deployment.status === "error";
return (
<div
@@ -370,6 +379,33 @@ export const ShowDeployments = ({
View
</Button>
{canDelete && (
<DialogAction
title="Delete Deployment"
description="Are you sure you want to delete this deployment? This action cannot be undone."
type="default"
onClick={async () => {
try {
await removeDeployment({
deploymentId: deployment.deploymentId,
});
toast.success("Deployment deleted successfully");
} catch (error) {
toast.error("Error deleting deployment");
}
}}
>
<Button
variant="destructive"
size="sm"
isLoading={isRemovingDeployment}
>
Delete
<Trash2 className="size-4" />
</Button>
</DialogAction>
)}
{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -159,11 +159,11 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
},
);
const { mutateAsync, isError, error, isLoading } = domainId
const { mutateAsync, isError, error, isPending } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
const { mutateAsync: generateDomain, isPending: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const { data: canGenerateTraefikMeDomains } =
@@ -240,7 +240,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
domainType: type,
});
}
}, [form, data, isLoading, domainId]);
}, [form, data, isPending, domainId]);
// Separate effect for handling custom cert resolver validation
useEffect(() => {
@@ -730,7 +730,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
</form>
<DialogFooter>
<Button isLoading={isLoading} form="hook-form" type="submit">
<Button isLoading={isPending} form="hook-form" type="submit">
{dictionary.submit}
</Button>
</DialogFooter>

View File

@@ -97,7 +97,7 @@ export const ShowDomains = ({ id, type }: Props) => {
const { mutateAsync: validateDomain } =
api.domain.validateDomain.useMutation();
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
const { mutateAsync: deleteDomain, isPending: isRemoving } =
api.domain.delete.useMutation();
const handleValidateDomain = async (host: string) => {

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type CSSProperties, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -60,7 +60,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
mongo: () => api.mongo.update.useMutation(),
compose: () => api.compose.update.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
@@ -111,7 +111,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -121,7 +121,7 @@ export const ShowEnvironment = ({ id, type }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [form, onSubmit, isLoading]);
}, [form, onSubmit, isPending]);
return (
<div className="flex w-full flex-col gap-5 ">
@@ -196,7 +196,7 @@ PORT=3000
</Button>
)}
<Button
isLoading={isLoading}
isLoading={isPending}
className="w-fit"
type="submit"
disabled={!hasChanges}

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -31,7 +31,7 @@ interface Props {
}
export const ShowEnvironment = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } =
const { mutateAsync, isPending } =
api.application.saveEnvironment.useMutation();
const { data, refetch } = api.application.one.useQuery(
@@ -104,7 +104,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -114,7 +114,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [form, onSubmit, isLoading]);
}, [form, onSubmit, isPending]);
return (
<Card className="bg-background px-6 pb-6">
@@ -214,7 +214,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
</Button>
)}
<Button
isLoading={isLoading}
isLoading={isPending}
className="w-fit"
type="submit"
disabled={!hasChanges}

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -54,6 +54,7 @@ const BitbucketProviderSchema = z.object({
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
slug: z.string().optional(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
@@ -73,15 +74,16 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
const { mutateAsync, isPending: isSavingBitbucketProvider } =
api.application.saveBitbucketProvider.useMutation();
const form = useForm<BitbucketProvider>({
const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
owner: "",
repo: "",
slug: "",
},
bitbucketId: "",
branch: "",
@@ -114,11 +116,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
} = api.bitbucket.getBitbucketBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
repo: repository?.slug || repository?.repo || "",
bitbucketId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
enabled:
!!repository?.owner &&
!!(repository?.slug || repository?.repo) &&
!!bitbucketId,
},
);
@@ -129,6 +134,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
repository: {
repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "",
slug: data.bitbucketRepositorySlug || "",
},
buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "",
@@ -142,6 +148,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
await mutateAsync({
bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo,
bitbucketRepositorySlug: data.repository.slug || data.repository.repo,
bitbucketOwner: data.repository.owner,
bitbucketBuildPath: data.buildPath,
bitbucketId: data.bitbucketId,
@@ -181,6 +188,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("repository", {
owner: "",
repo: "",
slug: "",
});
form.setValue("branch", "");
}}
@@ -217,7 +225,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && (
<Link
href={`https://bitbucket.org/${field.value.owner}/${field.value.repo}`}
href={`https://bitbucket.org/${field.value.owner}/${field.value.slug || field.value.repo}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
@@ -237,13 +245,13 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -255,11 +263,15 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!bitbucketId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a Bitbucket account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
@@ -271,6 +283,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
slug: repo.slug,
});
form.setValue("branch", "");
}}
@@ -320,7 +333,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
{status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -337,7 +350,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
{status === "pending" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { TrashIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -24,10 +24,10 @@ interface Props {
export const SaveDragNDrop = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading } =
const { mutateAsync, isPending } =
api.application.dropDeployment.useMutation();
const form = useForm<UploadFile>({
const form = useForm({
defaultValues: {},
resolver: zodResolver(uploadFileSchema),
});
@@ -129,8 +129,8 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
<Button
type="submit"
className="w-fit"
isLoading={isLoading}
disabled={!zip || isLoading}
isLoading={isPending}
disabled={!zip || isPending}
>
Deploy{" "}
</Button>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -58,10 +58,10 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
const { mutateAsync, isLoading } =
const { mutateAsync, isPending } =
api.application.saveGitProvider.useMutation();
const form = useForm<GitProvider>({
const form = useForm({
defaultValues: {
branch: "",
buildPath: "/",
@@ -317,7 +317,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</div>
<div className="flex flex-row justify-end">
<Button type="submit" className="w-fit" isLoading={isLoading}>
<Button type="submit" className="w-fit" isLoading={isPending}>
Save
</Button>
</div>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -88,10 +88,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGiteaProvider } =
const { mutateAsync, isPending: isSavingGiteaProvider } =
api.application.saveGiteaProvider.useMutation();
const form = useForm<GiteaProvider>({
const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -258,14 +258,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo: GiteaRepository) =>
repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -277,11 +277,15 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!giteaId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a Gitea account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
@@ -349,7 +353,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
{status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -367,7 +371,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
{status === "pending" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
@@ -459,7 +463,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<X
className="size-3 cursor-pointer hover:text-destructive"
onClick={() => {
const newPaths = [...field.value];
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
field.onChange(newPaths);
}}
@@ -477,7 +481,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
const input = e.currentTarget;
const path = input.value.trim();
if (path) {
field.onChange([...field.value, path]);
field.onChange([...(field.value || []), path]);
input.value = "";
}
}
@@ -494,7 +498,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
field.onChange([...field.value, path]);
field.onChange([...(field.value || []), path]);
input.value = "";
}
}}

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGithubProvider } =
const { mutateAsync, isPending: isSavingGithubProvider } =
api.application.saveGithubProvider.useMutation();
const form = useForm<GithubProvider>({
const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -94,7 +94,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
const { data: repositories, isLoading: isLoadingRepositories } =
const { data: repositories, isPending: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery(
{
githubId,
@@ -233,13 +233,13 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -251,11 +251,15 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!githubId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a GitHub account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
@@ -316,7 +320,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
{status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -333,7 +337,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
{status === "pending" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
@@ -455,7 +459,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge
key={index}
key={`${path}-${index}`}
variant="secondary"
className="flex items-center gap-1"
>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
@@ -74,10 +74,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGitlabProvider } =
const { mutateAsync, isPending: isSavingGitlabProvider } =
api.application.saveGitlabProvider.useMutation();
const form = useForm<GitlabProvider>({
const form = useForm({
defaultValues: {
buildPath: "/",
repository: {
@@ -254,13 +254,13 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -272,11 +272,15 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!gitlabId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a GitLab account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
@@ -347,7 +351,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
{status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -364,7 +368,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
{status === "pending" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
@@ -444,7 +448,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge
key={index}
key={`${path}-${index}`}
variant="secondary"
className="flex items-center gap-1"
>

View File

@@ -36,13 +36,13 @@ interface Props {
}
export const ShowProviderForm = ({ applicationId }: Props) => {
const { data: githubProviders, isLoading: isLoadingGithub } =
const { data: githubProviders, isPending: isLoadingGithub } =
api.github.githubProviders.useQuery();
const { data: gitlabProviders, isLoading: isLoadingGitlab } =
const { data: gitlabProviders, isPending: isLoadingGitlab } =
api.gitlab.gitlabProviders.useQuery();
const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
const { data: bitbucketProviders, isPending: isLoadingBitbucket } =
api.bitbucket.bitbucketProviders.useQuery();
const { data: giteaProviders, isLoading: isLoadingGitea } =
const { data: giteaProviders, isPending: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { data: application, refetch } = api.application.one.useQuery({

View File

@@ -37,14 +37,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
{ enabled: !!applicationId },
);
const { mutateAsync: update } = api.application.update.useMutation();
const { mutateAsync: start, isLoading: isStarting } =
const { mutateAsync: start, isPending: isStarting } =
api.application.start.useMutation();
const { mutateAsync: stop, isLoading: isStopping } =
const { mutateAsync: stop, isPending: isStopping } =
api.application.stop.useMutation();
const { mutateAsync: deploy } = api.application.deploy.useMutation();
const { mutateAsync: reload, isLoading: isReloading } =
const { mutateAsync: reload, isPending: isReloading } =
api.application.reload.useMutation();
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();

View File

@@ -34,6 +34,7 @@ export const DockerLogs = dynamic(
export const badgeStateColor = (state: string) => {
switch (state) {
case "running":
case "ready":
return "green";
case "exited":
case "shutdown":
@@ -55,7 +56,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const [containerId, setContainerId] = useState<string | undefined>();
const [option, setOption] = useState<"swarm" | "native">("native");
const { data: services, isLoading: servicesLoading } =
const { data: services, isPending: servicesLoading } =
api.docker.getServiceContainersByAppName.useQuery(
{
appName,
@@ -66,7 +67,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
},
);
const { data: containers, isLoading: containersLoading } =
const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,
@@ -142,6 +143,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
{container.status ? ` ${container.status}` : ""}
</SelectItem>
))}
</div>
@@ -157,6 +159,9 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
{container.currentState
? ` ${container.currentState}`
: ""}
</SelectItem>
))}
</>
@@ -166,6 +171,13 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
</SelectGroup>
</SelectContent>
</Select>
{option === "swarm" &&
services?.find((c) => c.containerId === containerId)?.error && (
<div className="rounded-md bg-destructive/10 border border-destructive/20 px-3 py-2 text-sm text-destructive">
<span className="font-medium">Error: </span>
{services?.find((c) => c.containerId === containerId)?.error}
</div>
)}
<DockerLogs
serverId={serverId || ""}
containerId={containerId || "select-a-container"}

View File

@@ -0,0 +1,107 @@
import { FilePlus } from "lucide-react";
import { useState } from "react";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
interface Props {
folderPath: string;
onCreate: (filename: string, content: string) => void;
onOpenChange: (open: boolean) => void;
alwaysVisible?: boolean;
}
export const CreateFileDialog = ({
folderPath,
onCreate,
onOpenChange,
alwaysVisible = false,
}: Props) => {
const [filename, setFilename] = useState("");
const [content, setContent] = useState("");
const handleCreate = () => {
if (!filename.trim()) return;
onCreate(filename.trim(), content);
setFilename("");
setContent("");
onOpenChange(false);
};
return (
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
type="button"
className={`h-6 w-6 ${alwaysVisible ? "" : "opacity-0 group-hover:opacity-100"}`}
title="Create file"
>
<FilePlus className="h-3 w-3" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl">
<form
onSubmit={(e) => {
e.preventDefault();
handleCreate();
}}
>
<DialogHeader>
<DialogTitle>Create file</DialogTitle>
<DialogDescription>
{folderPath ? `New file in ${folderPath}/` : "New file in root"}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="filename">Filename</Label>
<Input
id="filename"
placeholder="e.g. .env.example"
value={filename}
onChange={(e) => setFilename(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Content</Label>
<div className="h-[200px] rounded-md border">
<CodeEditor
value={content}
onChange={(v) => setContent(v ?? "")}
className="h-full"
wrapperClassName="h-[200px]"
lineWrapping
/>
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" type="button">
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button type="submit" disabled={!filename.trim()}>
Create
</Button>
</DialogClose>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,102 @@
import { Loader2, Pencil } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
interface Props {
patchId: string;
entityId: string;
type: "application" | "compose";
onSuccess?: () => void;
}
export const EditPatchDialog = ({
patchId,
entityId,
type,
onSuccess,
}: Props) => {
const { data: patch, isPending: isPatchLoading } = api.patch.one.useQuery(
{ patchId },
{ enabled: !!patchId },
);
const [content, setContent] = useState("");
useEffect(() => {
if (patch) {
setContent(patch.content);
}
}, [patch]);
const utils = api.useUtils();
const updatePatch = api.patch.update.useMutation();
const handleSave = () => {
updatePatch
.mutateAsync({ patchId, content })
.then(() => {
toast.success("Patch saved");
utils.patch.byEntityId.invalidate({ id: entityId, type });
onSuccess?.();
})
.catch((err) => {
toast.error(err.message);
});
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" title="Edit patch">
<Pencil className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-4xl max-h-[85vh] flex flex-col p-0">
<DialogHeader className="px-6 pt-6 pb-4">
<DialogTitle>Edit Patch</DialogTitle>
<DialogDescription>
{patch ? `Editing: ${patch.filePath}` : "Loading patch..."}
</DialogDescription>
</DialogHeader>
{isPatchLoading ? (
<div className="flex flex-1 items-center justify-center px-6 py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="flex-1 min-h-0 px-6 overflow-hidden flex flex-col">
<CodeEditor
value={content}
onChange={(value) => setContent(value ?? "")}
className="h-[400px] w-full"
wrapperClassName="h-[400px]"
lineWrapping
/>
</div>
)}
<DialogFooter className="px-6 ">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button onClick={handleSave} isLoading={updatePatch.isPending}>
{updatePatch.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,2 @@
export * from "./show-patches";
export * from "./patch-editor";

View File

@@ -0,0 +1,368 @@
import {
ArrowLeft,
ChevronRight,
File,
Folder,
Loader2,
Save,
Trash2,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { api } from "@/utils/api";
import { CreateFileDialog } from "./create-file-dialog";
interface Props {
id: string;
type: "application" | "compose";
repoPath: string;
onClose: () => void;
}
type DirectoryEntry = {
name: string;
path: string;
type: "file" | "directory";
children?: DirectoryEntry[];
};
export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [fileContent, setFileContent] = useState<string>("");
const [createFolderPath, setCreateFolderPath] = useState<string | null>(null);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
new Set(),
);
const utils = api.useUtils();
const { data: directories, isPending: isDirLoading } =
api.patch.readRepoDirectories.useQuery(
{ id: id, type, repoPath },
{ enabled: !!repoPath },
);
const { data: patches } = api.patch.byEntityId.useQuery(
{ id, type },
{ enabled: !!id },
);
const { mutateAsync: saveAsPatch, isPending: isSavingPatch } =
api.patch.saveFileAsPatch.useMutation();
const { mutateAsync: markForDeletion, isPending: isMarkingDeletion } =
api.patch.markFileForDeletion.useMutation();
const updatePatch = api.patch.update.useMutation();
const { data: fileData, isFetching: isFileLoading } =
api.patch.readRepoFile.useQuery(
{
id,
type,
filePath: selectedFile || "",
},
{
enabled: !!selectedFile,
},
);
useEffect(() => {
if (fileData !== undefined) {
setFileContent(fileData);
}
}, [fileData]);
const handleFileSelect = (filePath: string) => {
setSelectedFile(filePath);
};
const toggleFolder = (path: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev);
if (next.has(path)) {
next.delete(path);
} else {
next.add(path);
}
return next;
});
};
const handleSave = () => {
if (!selectedFile) return;
saveAsPatch({
id,
type,
filePath: selectedFile,
content: fileContent,
patchType: "update",
})
.then(() => {
toast.success("Patch saved");
utils.patch.byEntityId.invalidate({ id, type });
})
.catch(() => {
toast.error("Failed to save patch");
});
};
const handleMarkForDeletion = () => {
if (!selectedFile) return;
markForDeletion({ id, type, filePath: selectedFile })
.then(() => {
toast.success("File marked for deletion");
utils.patch.byEntityId.invalidate({ id, type });
})
.catch(() => {
toast.error("Failed to mark file for deletion");
});
};
const handleCreateFile = useCallback(
(folderPath: string, filename: string, content: string) => {
const filePath = folderPath ? `${folderPath}/${filename}` : filename;
saveAsPatch({
id,
type,
filePath,
content,
patchType: "create",
})
.then(() => {
toast.success("File created");
utils.patch.byEntityId.invalidate({ id, type });
})
.catch(() => {
toast.error("Failed to create file");
});
},
[id, type, saveAsPatch, utils],
);
const selectedFilePatch = patches?.find(
(p) => p.filePath === selectedFile && p.type === "delete",
);
const handleUnmarkDeletion = () => {
if (!selectedFilePatch) return;
updatePatch
.mutateAsync({
patchId: selectedFilePatch.patchId,
type: "update",
content: fileData || "",
})
.then(() => {
toast.success("Deletion unmarked");
utils.patch.byEntityId.invalidate({ id, type });
})
.catch(() => {
toast.error("Failed to unmark deletion");
});
};
const hasChanges = fileData !== undefined && fileContent !== fileData;
const renderTree = useCallback(
(entries: DirectoryEntry[], depth = 0) => {
return entries
.sort((a, b) => {
// Directories first, then alphabetically
if (a.type !== b.type) {
return a.type === "directory" ? -1 : 1;
}
return a.name.localeCompare(b.name);
})
.map((entry) => {
const isExpanded = expandedFolders.has(entry.path);
const isSelected = selectedFile === entry.path;
if (entry.type === "directory") {
return (
<div key={entry.path}>
<div className="group flex items-center">
<button
type="button"
onClick={() => toggleFolder(entry.path)}
className={
"flex-1 flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted/50 rounded-md transition-colors text-left min-w-0"
}
style={{ paddingLeft: `${depth * 12 + 8}px` }}
>
<ChevronRight
className={`h-4 w-4 shrink-0 transition-transform ${
isExpanded ? "rotate-90" : ""
}`}
/>
<Folder className="h-4 w-4 shrink-0 text-blue-500" />
<span className="truncate">{entry.name}</span>
</button>
<CreateFileDialog
folderPath={entry.path}
onCreate={(filename, content) =>
handleCreateFile(entry.path, filename, content)
}
onOpenChange={(open) =>
setCreateFolderPath(open ? entry.path : null)
}
/>
</div>
{isExpanded && entry.children && (
<div>{renderTree(entry.children, depth + 1)}</div>
)}
</div>
);
}
const isMarkedForDeletion = patches?.some(
(p) => p.filePath === entry.path && p.type === "delete",
);
return (
<button
type="button"
key={entry.path}
onClick={() => handleFileSelect(entry.path)}
className={`w-full flex items-center gap-2 px-2 py-1.5 text-sm hover:bg-muted/50 rounded-md transition-colors ${
isSelected ? "bg-muted" : ""
} ${isMarkedForDeletion ? "text-destructive" : ""}`}
style={{ paddingLeft: `${depth * 12 + 28}px` }}
>
<File className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{entry.name}</span>
{isMarkedForDeletion && (
<Trash2 className="h-3 w-3 shrink-0 text-destructive ml-auto" />
)}
</button>
);
});
},
[expandedFolders, selectedFile, patches, handleCreateFile],
);
return (
<Card className="bg-background overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between pb-4">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={onClose}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<CardTitle>Edit File</CardTitle>
<CardDescription>
{selectedFile
? `Editing: ${selectedFile}`
: "Select a file from the tree to edit"}
</CardDescription>
</div>
</div>
{selectedFile && (
<div className="flex items-center gap-2">
{selectedFilePatch ? (
<Button
variant="outline"
size="sm"
onClick={handleUnmarkDeletion}
disabled={updatePatch.isPending}
>
{updatePatch.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Unmark deletion
</Button>
) : (
<>
<Button
variant="outline"
size="sm"
onClick={handleMarkForDeletion}
disabled={isMarkingDeletion}
>
{isMarkingDeletion && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
<Trash2 className="mr-2 h-4 w-4" />
Mark for deletion
</Button>
<Button
onClick={handleSave}
disabled={isSavingPatch || !hasChanges}
>
{isSavingPatch && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
<Save className="mr-2 h-4 w-4" />
Save Patch
</Button>
</>
)}
</div>
)}
</CardHeader>
<CardContent className="p-0">
<div className="grid grid-cols-[250px_1fr] border-t h-[600px]">
<div className="border-r h-full overflow-hidden">
<ScrollArea className="h-full">
<div className="p-2 space-y-1">
<div className="group flex items-center gap-2 px-2 py-1.5 mb-1">
<CreateFileDialog
folderPath=""
alwaysVisible
onCreate={(filename, content) =>
handleCreateFile("", filename, content)
}
onOpenChange={(open) =>
setCreateFolderPath(open ? "" : null)
}
/>
<span className="text-xs text-muted-foreground">
New file in root
</span>
</div>
{isDirLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : directories ? (
renderTree(directories)
) : (
<div className="text-sm text-muted-foreground p-4">
No files found
</div>
)}
</div>
</ScrollArea>
</div>
<div className="h-full overflow-hidden relative">
{isFileLoading ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : selectedFile ? (
<CodeEditor
value={fileData || ""}
onChange={(value) => setFileContent(value || "")}
className="h-full w-full"
wrapperClassName="h-full"
lineWrapping
/>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">
Select a file to edit
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,225 @@
import { File, FilePlus2, Loader2, Trash2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
import { EditPatchDialog } from "./edit-patch-dialog";
import { PatchEditor } from "./patch-editor";
interface Props {
id: string;
type: "application" | "compose";
}
export const ShowPatches = ({ id, type }: Props) => {
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [repoPath, setRepoPath] = useState<string | null>(null);
const [isLoadingRepo, setIsLoadingRepo] = useState(false);
const utils = api.useUtils();
const { data: patches, isPending: isPatchesLoading } =
api.patch.byEntityId.useQuery({ id, type }, { enabled: !!id });
const mutationMap = {
application: () => api.patch.delete.useMutation(),
compose: () => api.patch.delete.useMutation(),
};
const ensureRepo = api.patch.ensureRepo.useMutation();
const togglePatch = api.patch.toggleEnabled.useMutation();
const { mutateAsync } = mutationMap[type]
? mutationMap[type]()
: api.patch.delete.useMutation();
const handleCloseEditor = () => {
setSelectedFile(null);
setRepoPath(null);
};
if (repoPath) {
return (
<PatchEditor
id={id}
type={type}
repoPath={repoPath || ""}
onClose={handleCloseEditor}
/>
);
}
const handleOpenEditor = async () => {
setIsLoadingRepo(true);
await ensureRepo
.mutateAsync({ id, type })
.then((result) => {
setRepoPath(result);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setIsLoadingRepo(false);
});
};
return (
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Patches</CardTitle>
<CardDescription>
Apply code patches to your repository during build. Patches are
applied after cloning the repository and before building.
</CardDescription>
</div>
{patches && patches?.length > 0 && (
<Button onClick={handleOpenEditor} disabled={isLoadingRepo}>
{isLoadingRepo && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<FilePlus2 className="mr-2 h-4 w-4" />
Create Patch
</Button>
)}
</CardHeader>
<CardContent>
{isPatchesLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : patches?.length === 0 ? (
<div className="flex min-h-[40vh] w-full flex-col items-center justify-center gap-4 rounded-lg border border-dashed p-8">
<div className="rounded-full bg-muted p-4">
<FilePlus2 className="h-10 w-10 text-muted-foreground" />
</div>
<div className="space-y-1 text-center">
<p className="text-sm font-medium">No patches yet</p>
<p className="max-w-sm text-sm text-muted-foreground">
Add file patches to modify your repo before each buildconfigs,
env, or code. Create your first patch to get started.
</p>
</div>
<Button onClick={handleOpenEditor} disabled={isLoadingRepo}>
{isLoadingRepo && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
<FilePlus2 className="mr-2 h-4 w-4" />
Create Patch
</Button>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>File Path</TableHead>
<TableHead className="w-[80px]">Type</TableHead>
<TableHead className="w-[100px]">Enabled</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{patches?.map((patch) => (
<TableRow key={patch.patchId}>
<TableCell className="font-mono text-sm">
<div className="flex items-center gap-2">
<File className="h-4 w-4 text-muted-foreground shrink-0" />
{patch.filePath}
</div>
</TableCell>
<TableCell>
<Badge
variant={
patch.type === "delete"
? "destructive"
: patch.type === "create"
? "default"
: "secondary"
}
className="font-normal"
>
{patch.type}
</Badge>
</TableCell>
<TableCell>
<Switch
checked={patch.enabled}
onCheckedChange={(checked) => {
togglePatch
.mutateAsync({
patchId: patch.patchId,
enabled: checked,
})
.then(() => {
toast.success("Patch updated");
utils.patch.byEntityId.invalidate({
id,
type,
});
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setIsLoadingRepo(false);
});
}}
/>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
{(patch.type === "update" || patch.type === "create") && (
<EditPatchDialog
patchId={patch.patchId}
entityId={id}
type={type}
/>
)}
<Button
variant="ghost"
size="icon"
onClick={() => {
mutateAsync({ patchId: patch.patchId })
.then(() => {
toast.success("Patch deleted");
utils.patch.byEntityId.invalidate({
id,
type,
});
})
.catch((err) => {
toast.error(err.message);
});
}}
title="Delete patch"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
);
};

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Dices } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -75,11 +75,11 @@ export const AddPreviewDomain = ({
},
);
const { mutateAsync, isError, error, isLoading } = domainId
const { mutateAsync, isError, error, isPending } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
const { mutateAsync: generateDomain, isPending: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const form = useForm<Domain>({
@@ -103,7 +103,7 @@ export const AddPreviewDomain = ({
if (!domainId) {
form.reset({});
}
}, [form, form.reset, data, isLoading]);
}, [form, form.reset, data, isPending]);
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
@@ -301,7 +301,7 @@ export const AddPreviewDomain = ({
</form>
<DialogFooter>
<Button isLoading={isLoading} form="hook-form" type="submit">
<Button isLoading={isPending} form="hook-form" type="submit">
{dictionary.submit}
</Button>
</DialogFooter>

View File

@@ -43,7 +43,7 @@ interface Props {
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery({ applicationId });
const { mutateAsync: deletePreviewDeployment, isLoading } =
const { mutateAsync: deletePreviewDeployment, isPending } =
api.previewDeployment.delete.useMutation();
const { mutateAsync: redeployPreviewDeployment } =
@@ -57,8 +57,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
{ applicationId },
{
enabled: !!applicationId,
refetchInterval: (data) =>
data?.some((d) => d.previewStatus === "running") ? 2000 : false,
refetchInterval: 2000,
},
);
@@ -282,7 +281,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
<Button
variant="ghost"
size="sm"
isLoading={isLoading}
isLoading={isPending}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="size-4" />

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -80,7 +80,7 @@ interface Props {
export const ShowPreviewSettings = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
const { mutateAsync: updateApplication, isLoading } =
const { mutateAsync: updateApplication, isPending } =
api.application.update.useMutation();
const { data, refetch } = api.application.one.useQuery({ applicationId });
@@ -535,7 +535,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
Cancel
</Button>
<Button
isLoading={isLoading}
isLoading={isPending}
form="hook-form-delete-application"
type="submit"
>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -71,7 +71,7 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
},
);
const { mutateAsync: updateApplication, isLoading } =
const { mutateAsync: updateApplication, isPending } =
api.application.update.useMutation();
const { data: registries } = api.registry.all.useQuery();
@@ -212,7 +212,7 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
/>
)}
<Button type="submit" className="w-full" isLoading={isLoading}>
<Button type="submit" className="w-full" isLoading={isPending}>
Save Settings
</Button>
</form>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import {
CheckIcon,
ChevronsUpDown,
@@ -220,8 +220,8 @@ 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),
const form = useForm({
resolver: standardSchemaResolver(formSchema),
defaultValues: {
name: "",
cronExpression: "",
@@ -275,11 +275,11 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
}
}, [form, schedule, scheduleId]);
const { mutateAsync, isLoading } = scheduleId
const { mutateAsync, isPending } = scheduleId
? api.schedule.update.useMutation()
: api.schedule.create.useMutation();
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const onSubmit = async (values: z.output<typeof formSchema>) => {
if (!id && !scheduleId) return;
await mutateAsync({
@@ -662,7 +662,7 @@ echo "Hello, world!"
)}
/>
<Button type="submit" isLoading={isLoading} className="w-full">
<Button type="submit" isLoading={isPending} className="w-full">
{scheduleId ? "Update" : "Create"} Schedule
</Button>
</form>

View File

@@ -51,7 +51,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
},
);
const utils = api.useUtils();
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
const { mutateAsync: deleteSchedule, isPending: isDeleting } =
api.schedule.delete.useMutation();
const { mutateAsync: runManually } = api.schedule.runManually.useMutation();

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -43,7 +43,7 @@ interface Props {
export const UpdateApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
const { mutateAsync, error, isError, isPending } =
api.application.update.useMutation();
const { data } = api.application.one.useQuery(
{
@@ -148,7 +148,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
/>
<DialogFooter>
<Button
isLoading={isLoading}
isLoading={isPending}
form="hook-form-update-application"
type="submit"
>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -116,7 +116,7 @@ export const HandleVolumeBackups = ({
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
const utils = api.useUtils();
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
@@ -195,7 +195,7 @@ export const HandleVolumeBackups = ({
}
}, [form, volumeBackup, volumeBackupId]);
const { mutateAsync, isLoading } = volumeBackupId
const { mutateAsync, isPending } = volumeBackupId
? api.volumeBackups.update.useMutation()
: api.volumeBackups.create.useMutation();
@@ -207,7 +207,7 @@ export const HandleVolumeBackups = ({
await mutateAsync({
...values,
keepLatestCount: preparedKeepLatestCount,
keepLatestCount: preparedKeepLatestCount ?? undefined,
destinationId: values.destinationId,
volumeBackupId: volumeBackupId || "",
serviceType: volumeBackupType,
@@ -630,7 +630,7 @@ export const HandleVolumeBackups = ({
)}
/>
<Button type="submit" isLoading={isLoading} className="w-full">
<Button type="submit" isLoading={isPending} className="w-full">
{volumeBackupId ? "Update" : "Create"} Volume Backup
</Button>
</form>

View File

@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
import { debounce } from "lodash";
import debounce from "lodash/debounce";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -53,27 +53,15 @@ interface Props {
}
const RestoreBackupSchema = z.object({
destinationId: z
.string({
required_error: "Please select a destination",
})
.min(1, {
message: "Destination is required",
}),
backupFile: z
.string({
required_error: "Please select a backup file",
})
.min(1, {
message: "Backup file is required",
}),
volumeName: z
.string({
required_error: "Please enter a volume name",
})
.min(1, {
message: "Volume name is required",
}),
destinationId: z.string().min(1, {
message: "Destination is required",
}),
backupFile: z.string().min(1, {
message: "Backup file is required",
}),
volumeName: z.string().min(1, {
message: "Volume name is required",
}),
});
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
@@ -83,7 +71,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
const { data: destinations = [] } = api.destination.all.useQuery();
const form = useForm<z.infer<typeof RestoreBackupSchema>>({
const form = useForm({
defaultValues: {
destinationId: "",
backupFile: "",
@@ -105,7 +93,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
debouncedSetSearch(value);
};
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
{
destinationId: destinationId,
search: debouncedSearchTerm,
@@ -294,7 +282,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
onValueChange={handleSearchChange}
className="h-9"
/>
{isLoading ? (
{isPending ? (
<div className="py-6 text-center text-sm">
Loading backup files...
</div>

View File

@@ -54,7 +54,7 @@ export const ShowVolumeBackups = ({
},
);
const utils = api.useUtils();
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
const { mutateAsync: deleteVolumeBackup, isPending: isDeleting } =
api.volumeBackups.delete.useMutation();
const { mutateAsync: runManually } =
api.volumeBackups.runManually.useMutation();

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -52,7 +52,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isLoading } = api.compose.update.useMutation();
const { mutateAsync, isPending } = api.compose.update.useMutation();
const form = useForm<AddCommand>({
defaultValues: {
@@ -128,7 +128,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
/>
</div>
<div className="flex justify-end">
<Button isLoading={isLoading} type="submit" className="w-fit">
<Button isLoading={isPending} type="submit" className="w-fit">
Save
</Button>
</div>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { AlertTriangle, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";

View File

@@ -1,5 +1,5 @@
import type { ServiceType } from "@dokploy/server/db/schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { useRouter } from "next/router";
@@ -74,7 +74,7 @@ export const DeleteService = ({ id, type }: Props) => {
mongo: () => api.mongo.remove.useMutation(),
compose: () => api.compose.delete.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
const { mutateAsync, isPending } = mutationMap[type]
? mutationMap[type]()
: api.mongo.remove.useMutation();
const { push } = useRouter();
@@ -130,7 +130,7 @@ export const DeleteService = ({ id, type }: Props) => {
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isLoading}
isLoading={isPending}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
@@ -228,7 +228,7 @@ export const DeleteService = ({ id, type }: Props) => {
</Button>
<Button
isLoading={isLoading}
isLoading={isPending}
disabled={isDisabled}
form="hook-form-delete-compose"
type="submit"

View File

@@ -28,9 +28,9 @@ export const ComposeActions = ({ composeId }: Props) => {
const { mutateAsync: update } = api.compose.update.useMutation();
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
const { mutateAsync: start, isLoading: isStarting } =
const { mutateAsync: start, isPending: isStarting } =
api.compose.start.useMutation();
const { mutateAsync: stop, isLoading: isStopping } =
const { mutateAsync: stop, isPending: isStopping } =
api.compose.stop.useMutation();
return (
<div className="flex flex-row gap-4 w-full flex-wrap ">

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -34,7 +34,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
{ enabled: !!composeId },
);
const { mutateAsync, isLoading } = api.compose.update.useMutation();
const { mutateAsync, isPending } = api.compose.update.useMutation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const form = useForm<AddComposeFile>({
@@ -93,7 +93,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isPending) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
@@ -103,7 +103,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [form, onSubmit, isLoading]);
}, [form, onSubmit, isPending]);
return (
<>
@@ -167,7 +167,7 @@ services:
<Button
type="submit"
form="hook-form-save-compose-file"
isLoading={isLoading}
isLoading={isPending}
className="lg:w-fit w-full"
>
Save

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -54,6 +54,7 @@ const BitbucketProviderSchema = z.object({
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
slug: z.string().optional(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
@@ -73,15 +74,16 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
const { mutateAsync, isPending: isSavingBitbucketProvider } =
api.compose.update.useMutation();
const form = useForm<BitbucketProvider>({
const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
owner: "",
repo: "",
slug: "",
},
bitbucketId: "",
branch: "",
@@ -114,11 +116,14 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
} = api.bitbucket.getBitbucketBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
repo: repository?.slug || repository?.repo || "",
bitbucketId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
enabled:
!!repository?.owner &&
!!(repository?.slug || repository?.repo) &&
!!bitbucketId,
},
);
@@ -129,6 +134,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
repository: {
repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "",
slug: data.bitbucketRepositorySlug || "",
},
composePath: data.composePath,
bitbucketId: data.bitbucketId || "",
@@ -142,6 +148,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
await mutateAsync({
bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo,
bitbucketRepositorySlug: data.repository.slug || data.repository.repo,
bitbucketOwner: data.repository.owner,
bitbucketId: data.bitbucketId,
composePath: data.composePath,
@@ -183,6 +190,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("repository", {
owner: "",
repo: "",
slug: "",
});
form.setValue("branch", "");
}}
@@ -219,7 +227,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && (
<Link
href={`https://bitbucket.org/${field.value.owner}/${field.value.repo}`}
href={`https://bitbucket.org/${field.value.owner}/${field.value.slug || field.value.repo}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
@@ -239,13 +247,13 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
{!field.value.owner
? "Select repository"
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@@ -257,11 +265,15 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
{!bitbucketId ? (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a Bitbucket account first
</span>
) : isLoadingRepositories ? (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
) : null}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
@@ -273,6 +285,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
slug: repo.slug,
});
form.setValue("branch", "");
}}
@@ -322,7 +335,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
{status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -339,7 +352,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
{status === "pending" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -58,9 +58,9 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
const { mutateAsync, isLoading } = api.compose.update.useMutation();
const { mutateAsync, isPending } = api.compose.update.useMutation();
const form = useForm<GitProvider>({
const form = useForm({
defaultValues: {
branch: "",
repositoryURL: "",
@@ -318,7 +318,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
</div>
<div className="flex flex-row justify-end">
<Button type="submit" className="w-fit" isLoading={isLoading}>
<Button type="submit" className="w-fit" isLoading={isPending}>
Save{" "}
</Button>
</div>

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