Compare commits

..

234 Commits

Author SHA1 Message Date
Mauricio Siu
6a7da40ffe Merge branch 'canary' into feat/add-custom-dokploy-loader 2026-03-01 15:53:33 -06:00
Mauricio Siu
39b40c58bb Merge pull request #3838 from lklacar/fix/service-card-behavior
fix: Fixed service card behavior #3837
2026-03-01 15:38:51 -06:00
Mauricio Siu
1861e10b2a Merge pull request #3859 from Dokploy/3851-ui-3-issues-in-1
feat: enhance request logging display with formatted status and duration
2026-03-01 15:23:49 -06:00
Mauricio Siu
964e3c4150 feat: enhance request logging display with formatted status and duration
Added helper functions to format status labels and execution durations in the requests dashboard. Updated the display logic to show "N/A" for zero status and improved duration representation in microseconds and milliseconds. This enhances the clarity and usability of the request logs for better monitoring and analysis.
2026-03-01 15:11:41 -06:00
Mauricio Siu
e05f31d8c6 Merge pull request #3857 from Dokploy/feat/improve-queries
feat: enhance project and environment services with additional column…
2026-03-01 14:24:16 -06:00
Mauricio Siu
cc3b902d1e feat: include project name in API response columns
Added the 'name' column to the project API response structure to enhance the data returned for project queries. This change improves the clarity and usability of the API by ensuring that project names are included in the response, facilitating better data handling for clients.
2026-03-01 14:20:08 -06:00
Mauricio Siu
6c1f2372ed refactor: clean up project dashboard and API response structure
Removed unused imports and redundant code in the project dashboard component to enhance readability. Updated the API project router to streamline the data structure by eliminating unnecessary domain retrievals, while ensuring essential application and compose details are still included. This refactor improves maintainability and optimizes data handling for the project management interface.
2026-03-01 14:15:47 -06:00
Mauricio Siu
7da69862e1 refactor: update project query to use permissions-aware endpoint
Replaced the existing project query with the new `allForPermissions` endpoint to enhance data retrieval for server monitoring settings. This change aligns with recent API enhancements aimed at improving permissions management.
2026-03-01 14:07:16 -06:00
autofix-ci[bot]
612e73bb80 [autofix.ci] apply automated fixes 2026-03-01 20:02:48 +00:00
Mauricio Siu
a360a259f5 feat: add admin-only endpoint for project permissions with detailed environment data
Introduced a new API endpoint `allForPermissions` to retrieve projects along with their environments and services specifically for admin users. This enhancement allows for a more comprehensive permissions UI by including detailed information about each environment and its associated applications, improving the overall user experience in managing permissions.
2026-03-01 14:02:00 -06:00
Mauricio Siu
149293f4d3 feat: enhance mysql configuration with specific column selections
Updated the mysql configuration in the environment service to include specific column selections for the server object. This change improves data structure clarity and allows for more precise data handling in future queries.
2026-03-01 13:57:17 -06:00
Mauricio Siu
a8a5e1c6f1 refactor: remove unused environment property in duplicateEnvironment function
Eliminated the 'env' property from the duplicateEnvironment function to streamline the code and improve clarity. This change enhances maintainability by removing unnecessary parameters.
2026-03-01 13:47:38 -06:00
Mauricio Siu
4ede21eda9 feat: enhance project and environment services with additional column selections
Updated project and environment services to include specific column selections for various database entities. This improves data retrieval efficiency and allows for more granular control over the returned data structure. Added columns for application, mariadb, mongo, mysql, postgres, redis, and compose entities, as well as enhancements to the environment query structure.
2026-03-01 13:42:34 -06:00
Mauricio Siu
e275e9162e Merge pull request #3846 from Dokploy/feat/add-more-endpoints-for-search
feat: add search functionality across multiple routers with member ac…
2026-03-01 01:27:38 -06:00
autofix-ci[bot]
60a6dc5fab [autofix.ci] apply automated fixes 2026-03-01 07:15:20 +00:00
Mauricio Siu
705c5bc1c9 feat: add search functionality across multiple routers with member access control
Implemented a search feature in application, compose, environment, mariadb, mongo, mysql, postgres, project, and redis routers. Each search allows filtering by various parameters and respects user permissions based on their role. The search queries utilize optimized conditions for efficient data retrieval.
2026-03-01 01:14:46 -06:00
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
Luka Klacar
f9dedd979e fix: Fixed service card behavior #3837 2026-02-28 23:12:31 +01: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
Mauricio Siu
7a1703a191 fix(show-registry): correct loading state condition for WhaleLoader display
- Updated the ShowRegistry component to display the WhaleLoader when loading is in progress, improving user experience during data fetching.
2026-02-08 02:27:01 -06:00
Mauricio Siu
f6af5daf5e refactor(animation): update whale-loader animations and styles
- Removed the deprecated "whale-draw" animation from the Tailwind CSS configuration.
- Added a new "whale-draw" keyframe animation in globals.css to enhance the whale-loader's visual effect.
- Updated the WhaleLoader component to utilize the new animation and adjusted stroke properties for improved rendering.
2026-02-08 02:26:49 -06:00
Mauricio Siu
452b9a3c78 feat(loader): add WhaleLoader component and animation
- Introduced a new WhaleLoader component that displays a loading animation using the Dokploy whale logo.
- Updated the Tailwind CSS configuration to include a new animation for the whale loader.
- Replaced the existing loading indicator in the ShowRegistry component with the new WhaleLoader for improved visual consistency.
2026-02-08 02:19:05 -06: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
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
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
407 changed files with 62332 additions and 30295 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

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

@@ -53,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
@@ -165,10 +165,11 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.
### Important Considerations for Pull Requests
- **Testing is Mandatory:** All Pull Requests **must be tested** 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 will be closed.** This policy ensures clean contributions and reduces the time maintainers spend reviewing untested or broken code.
- **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

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

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

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,7 +12,7 @@
"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.11.7",
"pino": "9.4.0",
@@ -20,18 +20,19 @@
"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.16.0",
"@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

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

@@ -1,4 +1,3 @@
import { existsSync } from "node:fs";
import path from "node:path";
import type { ApplicationNested } from "@dokploy/server";
@@ -9,17 +8,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const REAL_TEST_TIMEOUT = 180000; // 3 minutes
// Mock constants to avoid load error
vi.mock("@dokploy/server/constants", () => ({
paths: () => ({
LOGS_PATH: "/tmp/dokploy-test-real/logs",
APPLICATIONS_PATH: "/tmp/dokploy-test-real/applications",
PATCH_REPOS_PATH: "/tmp/dokploy-test-real/patch-repos",
}),
IS_CLOUD: false,
docker: {},
}));
// Mock ONLY database and notifications
vi.mock("@dokploy/server/db", () => {
const createChainableMock = (): any => {
@@ -27,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(),
@@ -41,6 +34,12 @@ vi.mock("@dokploy/server/db", () => {
applications: {
findFirst: vi.fn(),
},
patch: {
findMany: vi.fn().mockResolvedValue([]),
},
member: {
findMany: vi.fn().mockResolvedValue([]),
},
},
},
};
@@ -79,16 +78,6 @@ vi.mock("@dokploy/server/services/rollbacks", () => ({
createRollback: vi.fn(),
}));
vi.mock("@dokploy/server/services/patch", async (importOriginal) => {
const actual = await importOriginal<
typeof import("@dokploy/server/services/patch")
>();
return {
...actual,
findPatchesByApplicationId: vi.fn().mockResolvedValue([]),
};
});
// NOT mocked (executed for real):
// - execAsync
// - cloneGitRepository
@@ -100,11 +89,6 @@ import * as adminService from "@dokploy/server/services/admin";
import * as applicationService from "@dokploy/server/services/application";
import { deployApplication } from "@dokploy/server/services/application";
import * as deploymentService from "@dokploy/server/services/deployment";
import * as patchService from "@dokploy/server/services/patch";
import { generatePatch } from "@dokploy/server/services/patch";
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const createMockApplication = (
overrides: Partial<ApplicationNested> = {},
@@ -501,105 +485,6 @@ describe(
},
REAL_TEST_TIMEOUT,
);
it(
"should REALLY apply patches from database during deployment",
async () => {
// 1. Setup local temporary git repo
const tempRepo = await mkdtemp(join(tmpdir(), "real-patch-repo-"));
// Helper for local git commands
const execLocal = async (cmd: string) => execAsync(cmd, { cwd: tempRepo });
await execLocal("git init");
await execLocal("git config user.email 'test@dokploy.com'");
await execLocal("git config user.name 'Dokploy Test'");
// Create a simple Dockerfile and server script
// We use a simple python server to verify output
await writeFile(join(tempRepo, "app.py"), "print('Original App')\n");
await writeFile(
join(tempRepo, "Dockerfile"),
"FROM python:3.9-slim\nCOPY app.py .\nCMD [\"python\", \"app.py\"]\n",
);
await execLocal("git add .");
await execLocal("git commit -m 'Initial commit'");
// Ensure master/main branch exists (git init might create master or main depending on config)
// We force create a branch named 'main' to be consistent
await execLocal("git checkout -b main || git checkout main");
// 2. Mock Application to use this local repo
const patchAppName = `real-patch-app-${Date.now()}`;
const patchApp = createMockApplication({
appName: patchAppName,
buildType: "dockerfile",
customGitUrl: `file://${tempRepo}`,
customGitBranch: "main",
dockerfile: "Dockerfile",
});
currentAppName = patchAppName;
allTestAppNames.push(patchAppName);
// Setup standard mocks
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
patchApp as any,
);
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
patchApp as any,
);
// 3. Generate a patch
// We modify the file, generate patch, and then reset.
const newContent = "print('Patched App')\n";
const patchContent = await generatePatch({
codePath: tempRepo,
filePath: "app.py",
newContent,
serverId: null,
});
// 4. Mock patch service to return this patch
vi.mocked(patchService.findPatchesByApplicationId).mockResolvedValue([
{
patchId: "test-patch-1",
applicationId: "test-app-id",
composeId: null,
filePath: "app.py",
content: patchContent,
enabled: true,
createdAt: new Date().toISOString(),
} as any,
]);
console.log(`\n🚀 Testing deployment with patch: ${currentAppName}`);
// 5. Deploy
const result = await deployApplication({
applicationId: "test-app-id",
titleLog: "Real Patch Test",
descriptionLog: "Testing patch application",
});
expect(result).toBe(true);
// 6. Verify Log contains "Applying patch"
const { stdout: logContent } = await execAsync(
`cat ${currentDeployment.logPath}`,
);
// The implementation logs "Applying patch: ..."
expect(logContent).toContain("Applying patch");
expect(logContent).toContain("app.py");
console.log("✅ Verified patch execution logs");
// 7. Verify the deployed image contains the patched code
// We run the image and check output
const { stdout: runOutput } = await execAsync(
`docker run --rm ${patchAppName}`,
);
expect(runOutput.trim()).toBe("Patched App");
console.log("✅ Verified patched output:", runOutput.trim());
},
REAL_TEST_TIMEOUT,
);
},
REAL_TEST_TIMEOUT,
);

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,
}),
};
});
@@ -150,6 +154,176 @@ const baseApp: ApplicationNested = {
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 () => {
@@ -166,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

@@ -1,106 +0,0 @@
import { generatePatch } from "@dokploy/server/services/patch";
import { describe, expect, it, afterEach } from "vitest";
import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execAsyncLocal = promisify(exec);
describe("Patch System Integration", () => {
let tempDir: string;
afterEach(async () => {
if (tempDir) {
await rm(tempDir, { recursive: true, force: true });
}
});
it("should generate a patch that can be successfully applied via git", async () => {
// Setup repo
tempDir = await mkdtemp(join(tmpdir(), "dokploy-patch-test-"));
const fileName = "test.txt";
const filePath = join(tempDir, fileName);
await execAsyncLocal("git init", { cwd: tempDir });
await execAsyncLocal("git config user.email 'test@test.com'", { cwd: tempDir });
await execAsyncLocal("git config user.name 'Test'", { cwd: tempDir });
// Original content
await writeFile(filePath, "line1\nline2\n");
await execAsyncLocal(`git add ${fileName}`, { cwd: tempDir });
await execAsyncLocal("git commit -m 'init'", { cwd: tempDir });
// Generate patch (modify content)
const newContent = "line1\nline2\nline3\n";
const patchContent = await generatePatch({
codePath: tempDir,
filePath: fileName,
newContent,
serverId: null,
});
// Verify patch format
expect(patchContent.endsWith("\n")).toBe(true);
// Reset file (generatePatch does reset, but ensure it)
await execAsyncLocal("git checkout .", { cwd: tempDir });
const savedContent = await readFile(filePath, "utf-8");
expect(savedContent).toBe("line1\nline2\n");
// Apply patch verification
// We simulate what Deployment Service does: write patch to file and run git apply
const patchFile = join(tempDir, "changes.patch");
await writeFile(patchFile, patchContent);
try {
await execAsyncLocal(`git apply --whitespace=fix ${patchFile}`, { cwd: tempDir });
} catch (e: any) {
console.error("Git apply failed:", e.message);
console.log("Patch content:", JSON.stringify(patchContent));
throw e;
}
const appliedContent = await readFile(filePath, "utf-8");
expect(appliedContent).toBe(newContent);
});
it("should handle files created without trailing newline", async () => {
// Setup repo
tempDir = await mkdtemp(join(tmpdir(), "dokploy-patch-test-noline-"));
const fileName = "noline.txt";
const filePath = join(tempDir, fileName);
await execAsyncLocal("git init", { cwd: tempDir });
await execAsyncLocal("git config user.email 'test@test.com'", { cwd: tempDir });
await execAsyncLocal("git config user.name 'Test'", { cwd: tempDir });
// Original content WITHOUT newline
await writeFile(filePath, "line1");
await execAsyncLocal(`git add ${fileName}`, { cwd: tempDir });
await execAsyncLocal("git commit -m 'init'", { cwd: tempDir });
// Generate patch
const newContent = "line1\nline2";
const patchContent = await generatePatch({
codePath: tempDir,
filePath: fileName,
newContent,
serverId: null,
});
// Verify patch format
expect(patchContent.endsWith("\n")).toBe(true);
// Apply patch
const patchFile = join(tempDir, "changes.patch");
await writeFile(patchFile, patchContent);
await execAsyncLocal(`git apply --whitespace=fix ${patchFile}`, { cwd: tempDir });
const appliedContent = await readFile(filePath, "utf-8");
expect(appliedContent).toBe(newContent);
});
});

View File

@@ -12,7 +12,11 @@ vi.mock("@dokploy/server/db", () => {
chain.where = () => chain;
chain.values = () => chain;
chain.returning = () => Promise.resolve([{}]);
chain.then = undefined;
chain.from = () => chain;
chain.innerJoin = () => chain;
chain.then = (resolve: (value: unknown) => void) => {
resolve([]);
};
const tableMock = {
findFirst: vi.fn(() => Promise.resolve(undefined)),
@@ -21,7 +25,6 @@ vi.mock("@dokploy/server/db", () => {
update: vi.fn(() => chain),
delete: vi.fn(() => chain),
};
const createQueryMock = () => tableMock;
return {
db: {

View File

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

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

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

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

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

@@ -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 { 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,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { InfoIcon, Plus, Trash2 } from "lucide-react";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
@@ -128,11 +128,11 @@ 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: "",
@@ -452,6 +452,11 @@ export const ShowResources = ({ id, type }: Props) => {
min={-1}
placeholder="65535"
{...field}
value={
typeof field.value === "number"
? field.value
: ""
}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
@@ -475,6 +480,11 @@ export const ShowResources = ({ id, type }: Props) => {
min={-1}
placeholder="65535"
{...field}
value={
typeof field.value === "number"
? field.value
: ""
}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
@@ -507,7 +517,7 @@ export const ShowResources = ({ id, type }: Props) => {
</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,7 +25,6 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
@@ -69,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>({
@@ -126,7 +126,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
}}
>
<DialogTrigger asChild>
<Button isLoading={isLoading}>Modify</Button>
<Button isLoading={isPending}>Modify</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
@@ -198,7 +198,7 @@ routers:
</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";
@@ -74,10 +74,10 @@ 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: {
@@ -333,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(
@@ -350,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: {
@@ -353,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(
@@ -371,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>
@@ -463,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);
}}
@@ -481,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 = "";
}
}
@@ -498,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,
@@ -320,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(
@@ -337,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>
@@ -459,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: {
@@ -351,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(
@@ -368,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>
@@ -448,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

@@ -56,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,
@@ -67,7 +67,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
},
);
const { data: containers, isLoading: containersLoading } =
const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,

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

@@ -1,16 +1,30 @@
import { ArrowLeft, ChevronRight, File, Folder, Loader2, Save } from "lucide-react";
import { useCallback, useState } from "react";
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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { api } from "@/utils/api";
import type { RouterOutputs } from "@/utils/api";
import { CreateFileDialog } from "./create-file-dialog";
interface Props {
applicationId?: string;
composeId?: string;
id: string;
type: "application" | "compose";
repoPath: string;
onClose: () => void;
}
@@ -22,63 +36,52 @@ type DirectoryEntry = {
children?: DirectoryEntry[];
};
export const PatchEditor = ({
applicationId,
composeId,
repoPath,
onClose,
}: Props) => {
export const PatchEditor = ({ id, type, repoPath, onClose }: Props) => {
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [fileContent, setFileContent] = useState<string>("");
const [originalContent, setOriginalContent] = useState<string>("");
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
const [isSaving, setIsSaving] = useState(false);
const [createFolderPath, setCreateFolderPath] = useState<string | null>(null);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
new Set(),
);
// Fetch directory tree
const { data: directories, isLoading: isDirLoading } =
const utils = api.useUtils();
const { data: directories, isPending: isDirLoading } =
api.patch.readRepoDirectories.useQuery(
{ applicationId, composeId, repoPath },
{ id: id, type, repoPath },
{ enabled: !!repoPath },
);
// Save mutation
const saveAsPatch = api.patch.saveFileAsPatch.useMutation({
onSuccess: (result) => {
setIsSaving(false);
if (result.deleted) {
toast.success("No changes - patch removed");
} else {
toast.success("Patch saved");
}
setOriginalContent(fileContent);
},
onError: () => {
setIsSaving(false);
toast.error("Failed to save patch");
},
});
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();
// Read file content when selected
const { data: fileData, isFetching: isFileLoading } =
api.patch.readRepoFile.useQuery(
{
applicationId,
composeId,
repoPath,
id,
type,
filePath: selectedFile || "",
},
{
enabled: !!selectedFile,
onSuccess: (data) => {
setFileContent(data.content);
setOriginalContent(data.content);
if (data.patchError) {
toast.error(data.patchErrorMessage || "Failed to apply patch");
}
},
},
);
useEffect(() => {
if (fileData !== undefined) {
setFileContent(fileData);
}
}, [fileData]);
const handleFileSelect = (filePath: string) => {
setSelectedFile(filePath);
};
@@ -97,17 +100,77 @@ export const PatchEditor = ({
const handleSave = () => {
if (!selectedFile) return;
setIsSaving(true);
saveAsPatch.mutate({
applicationId,
composeId,
repoPath,
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 hasChanges = fileContent !== originalContent;
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) => {
@@ -126,19 +189,33 @@ export const PatchEditor = ({
if (entry.type === "directory") {
return (
<div key={entry.path}>
<button
onClick={() => toggleFolder(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`}
style={{ paddingLeft: `${depth * 12 + 8}px` }}
>
<ChevronRight
className={`h-4 w-4 transition-transform ${
isExpanded ? "rotate-90" : ""
}`}
<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)
}
/>
<Folder className="h-4 w-4 text-blue-500" />
<span className="truncate">{entry.name}</span>
</button>
</div>
{isExpanded && entry.children && (
<div>{renderTree(entry.children, depth + 1)}</div>
)}
@@ -146,22 +223,30 @@ export const PatchEditor = ({
);
}
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 text-muted-foreground" />
<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],
[expandedFolders, selectedFile, patches, handleCreateFile],
);
return (
@@ -181,19 +266,68 @@ export const PatchEditor = ({
</div>
</div>
{selectedFile && (
<Button onClick={handleSave} disabled={isSaving || !hasChanges}>
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<Save className="mr-2 h-4 w-4" />
Save Patch
</Button>
<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]">
{/* File Tree */}
<div className="border-r h-full overflow-hidden">
<ScrollArea className="h-full">
<div className="p-2">
<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" />
@@ -208,7 +342,6 @@ export const PatchEditor = ({
</div>
</ScrollArea>
</div>
{/* Editor */}
<div className="h-full overflow-hidden relative">
{isFileLoading ? (
<div className="flex items-center justify-center h-full">
@@ -216,7 +349,7 @@ export const PatchEditor = ({
</div>
) : selectedFile ? (
<CodeEditor
value={fileContent}
value={fileData || ""}
onChange={(value) => setFileContent(value || "")}
className="h-full w-full"
wrapperClassName="h-full"

View File

@@ -1,9 +1,15 @@
import { AlertCircle, ChevronRight, File, Folder, Loader2, Power, Trash2 } from "lucide-react";
import { File, FilePlus2, Loader2, Trash2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import {
Table,
@@ -14,185 +20,199 @@ import {
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
import type { RouterOutputs } from "@/utils/api";
import { EditPatchDialog } from "./edit-patch-dialog";
import { PatchEditor } from "./patch-editor";
interface Props {
applicationId?: string;
composeId?: string;
id: string;
type: "application" | "compose";
}
type Patch = RouterOutputs["patch"]["byApplicationId"][number];
export const ShowPatches = ({ applicationId, composeId }: Props) => {
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();
// Fetch patches
// Fetch patches
const { data: appPatches, isLoading: isAppPatchesLoading } =
api.patch.byApplicationId.useQuery(
{ applicationId: applicationId! },
{ enabled: !!applicationId },
);
const { data: patches, isPending: isPatchesLoading } =
api.patch.byEntityId.useQuery({ id, type }, { enabled: !!id });
const { data: composePatches, isLoading: isComposePatchesLoading } =
api.patch.byComposeId.useQuery(
{ composeId: composeId! },
{ enabled: !!composeId },
);
const patches = applicationId ? appPatches : composePatches;
const isPatchesLoading = applicationId
? isAppPatchesLoading
: isComposePatchesLoading;
// Mutations
const deletePatch = api.patch.delete.useMutation({
onSuccess: () => {
toast.success("Patch deleted");
if (applicationId) {
utils.patch.byApplicationId.invalidate({ applicationId });
} else if (composeId) {
utils.patch.byComposeId.invalidate({ composeId });
}
},
onError: () => {
toast.error("Failed to delete patch");
},
});
const togglePatch = api.patch.toggleEnabled.useMutation({
onSuccess: () => {
toast.success("Patch updated");
if (applicationId) {
utils.patch.byApplicationId.invalidate({ applicationId });
} else if (composeId) {
utils.patch.byComposeId.invalidate({ composeId });
}
},
onError: () => {
toast.error("Failed to update patch");
},
});
const mutationMap = {
application: () => api.patch.delete.useMutation(),
compose: () => api.patch.delete.useMutation(),
};
const ensureRepo = api.patch.ensureRepo.useMutation();
const handleOpenEditor = async () => {
setIsLoadingRepo(true);
const toastId = toast.loading("Syncing repository...");
ensureRepo.mutate(
{ applicationId, composeId },
{
onSuccess: (path) => {
setRepoPath(path);
setIsLoadingRepo(false);
toast.dismiss(toastId);
},
onError: () => {
setIsLoadingRepo(false);
toast.dismiss(toastId);
toast.error("Failed to load repository");
},
},
);
};
const togglePatch = api.patch.toggleEnabled.useMutation();
const handleDeletePatch = (patchId: string) => {
deletePatch.mutate({ patchId });
};
const handleTogglePatch = (patchId: string, enabled: boolean) => {
togglePatch.mutate({ patchId, enabled });
};
const { mutateAsync } = mutationMap[type]
? mutationMap[type]()
: api.patch.delete.useMutation();
const handleCloseEditor = () => {
setSelectedFile(null);
setRepoPath(null);
if (applicationId) {
utils.patch.byApplicationId.invalidate({ applicationId });
} else if (composeId) {
utils.patch.byComposeId.invalidate({ composeId });
}
};
if (repoPath) {
return (
<PatchEditor
applicationId={applicationId}
composeId={composeId}
repoPath={repoPath}
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.
Apply code patches to your repository during build. Patches are
applied after cloning the repository and before building.
</CardDescription>
</div>
<Button onClick={handleOpenEditor} disabled={isLoadingRepo}>
{isLoadingRepo && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Patch
</Button>
{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 || patches.length === 0 ? (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>No patches</AlertTitle>
<AlertDescription>
No patches have been created for this application yet. Click "Create Patch"
to add modifications to your code during build.
</AlertDescription>
</Alert>
) : 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-[80px]">Actions</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{patches.map((patch: Patch) => (
{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" />
<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) =>
handleTogglePatch(patch.patchId, checked)
}
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>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeletePatch(patch.patchId)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
<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>
))}

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";
@@ -74,10 +74,10 @@ 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: {
@@ -335,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(
@@ -352,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>

View File

@@ -1,4 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ interface Props {
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingGiteaProvider } =
const { mutateAsync, isPending: isSavingGiteaProvider } =
api.compose.update.useMutation();
const form = useForm<GiteaProvider>({
const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -331,7 +331,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
{status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(

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, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
@@ -72,10 +72,10 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingGithubProvider } =
const { mutateAsync, isPending: isSavingGithubProvider } =
api.compose.update.useMutation();
const form = useForm<GithubProvider>({
const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -94,7 +94,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository");
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,
@@ -321,7 +321,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
{status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -338,7 +338,7 @@ export const SaveGithubProviderCompose = ({ 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 { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
@@ -74,10 +74,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingGitlabProvider } =
const { mutateAsync, isPending: isSavingGitlabProvider } =
api.compose.update.useMutation();
const form = useForm<GitlabProvider>({
const form = useForm({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
@@ -353,7 +353,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
{status === "pending" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
@@ -370,7 +370,7 @@ export const SaveGitlabProviderCompose = ({ 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

@@ -27,13 +27,13 @@ interface Props {
}
export const ShowProviderFormCompose = ({ composeId }: 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 { mutateAsync: disconnectGitProvider } =

View File

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

View File

@@ -32,7 +32,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
},
);
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
const { mutateAsync, isPending } = api.compose.fetchSourceType.useMutation();
useEffect(() => {
if (isOpen) {
@@ -66,7 +66,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
</AlertBlock>
{isLoading ? (
{isPending ? (
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md">
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
</div>
@@ -82,7 +82,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
<div className="flex flex-row gap-2 justify-end my-4">
<Button
variant="secondary"
isLoading={isLoading}
isLoading={isPending}
onClick={() => {
mutateAsync({ composeId })
.then(() => {

View File

@@ -41,7 +41,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
const [option, setOption] = useState<"swarm" | "native">("native");
const [containerId, setContainerId] = useState<string | undefined>();
const { data: services, isLoading: servicesLoading } =
const { data: services, isPending: servicesLoading } =
api.docker.getStackContainersByAppName.useQuery(
{
appName,
@@ -52,7 +52,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
},
);
const { data: containers, isLoading: containersLoading } =
const { data: containers, isPending: containersLoading } =
api.docker.getContainersByAppNameMatch.useQuery(
{
appName,

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