Compare commits

...

190 Commits

Author SHA1 Message Date
Mauricio Siu edbdc01a1e chore(package): bump version to v0.24.9 2025-08-10 06:12:28 -06:00
Mauricio Siu 33d6c2073b Merge pull request #2337 from A-D-E/fix/gitlab-branches-pagination
fix: use configured GitLab URL instead of hardcoded gitlab.com
2025-08-09 00:23:43 -06:00
A-D-E 33873ce1e9 fix: use configured GitLab URL instead of hardcoded gitlab.com
The previous pagination implementation accidentally hardcoded the GitLab URL
to gitlab.com, breaking the integration for self-hosted GitLab instances.

Changes:
- Replace hardcoded 'https://gitlab.com' with gitlabProvider.gitlabUrl
- Maintains the pagination functionality added in previous commit

Fixes: GitLab branches API calls failing for self-hosted instances
2025-08-05 21:31:15 +02:00
Mauricio Siu 1758655f66 feat(swarm): add OpenAPI metadata for drop-deployment endpoint in swarm router 2025-08-04 00:28:03 -06:00
Mauricio Siu 029eed7755 refactor(dashboard): improve layout and spacing in ShowConvertedCompose component 2025-08-03 18:10:19 -06:00
Mauricio Siu f017536396 chore(package): bump version to v0.24.7 2025-08-03 18:04:24 -06:00
Mauricio Siu ba5505cf81 feat(auth): add logger configuration to disable logging in production environment 2025-08-03 18:04:09 -06:00
Mauricio Siu 5ff5da9ff9 Merge pull request #2321 from Dokploy/1738-database-placement-constraints
feat(cluster-settings): Add swarm settings for databases
2025-08-03 17:41:08 -06:00
Mauricio Siu e96a8ea4ad feat(databases): increment ForceUpdate in TaskTemplate for service updates across MariaDB, MongoDB, MySQL, Postgres, and Redis 2025-08-03 17:39:35 -06:00
Mauricio Siu 42864d2472 feat(docker): update generateConfigContainer to accept Partial<ApplicationNested> and enhance mount checks 2025-08-03 17:21:09 -06:00
Mauricio Siu ae25ea265c feat(databases): enhance database service configuration by integrating health checks, restart policies, and additional settings across MariaDB, MongoDB, MySQL, Postgres, and Redis 2025-08-03 16:52:43 -06:00
Mauricio Siu 0755f28307 refactor(cluster-settings): update dialog content and improve layout in swarm settings component 2025-08-03 16:45:07 -06:00
Mauricio Siu 3d10d48425 fix(cluster-settings): ensure data validation for registryId in form defaults 2025-08-03 16:40:24 -06:00
Mauricio Siu 6e79183f6a feat(cluster-settings): refactor cluster settings components to support multiple database types and add new swarm configuration fields 2025-08-03 16:36:14 -06:00
Mauricio Siu 2318fb062a Merge pull request #2320 from Dokploy/2004-how-to-pause-log-output-in-doploy
feat(docker-logs): add pause/resume functionality for log streaming
2025-08-03 15:45:54 -06:00
Mauricio Siu 14b4bc9d85 feat(docker-logs): add pause/resume functionality for log streaming with message buffering 2025-08-03 15:43:53 -06:00
Mauricio Siu 4c72f1894c Merge pull request #2319 from Dokploy/1614-sort-project-alphabetically-and-by-other-criteria
feat(show-projects): implement project sorting and filtering function…
2025-08-03 15:25:52 -06:00
Mauricio Siu ebd632df04 feat(show-projects): implement project sorting and filtering functionality 2025-08-03 15:24:56 -06:00
Mauricio Siu 4878ed2b6f Merge pull request #2318 from Dokploy/2223-local-deployment-doesnt-replace-old-container
feat(handle-ports): add publish mode warning for host mode limitations
2025-08-03 15:15:05 -06:00
Mauricio Siu 1d3ab2bafa feat(handle-ports): add publish mode warning for host mode limitations 2025-08-03 15:13:11 -06:00
Mauricio Siu 7cb7cfa2a8 feat(pr-template): add a pull request template to standardize submissions 2025-08-03 13:38:17 -06:00
Mauricio Siu 6009697710 Merge pull request #2312 from JamBalaya56562/create-pr
ci(create-pr): remove unused id
2025-08-03 13:30:29 -06:00
Mauricio Siu 6be86b49bb Merge pull request #2292 from JamBalaya56562/lint-shared
refactor: lint apps/components/shared files
2025-08-03 13:27:11 -06:00
Mauricio Siu 1e81244e0b docs(contributing): update pull request guidelines and clarify branch roles 2025-08-03 13:21:02 -06:00
Mauricio Siu f659ea463d Merge pull request #2311 from JamBalaya56562/contributing
docs(contributing): use Alerts syntax of GitHub markdown
2025-08-03 13:17:16 -06:00
JamBalaya56562 caf57276a4 ci(create-pr): remove unused id 2025-08-03 18:09:33 +09:00
JamBalaya56562 15c6c7e657 docs(contributing): use Alerts syntax of GitHub markdown 2025-08-03 17:50:29 +09:00
JamBalaya56562 8be0db385a refactor: lint apps/components/shared files 2025-08-03 17:25:06 +09:00
Mauricio Siu 4259e2533e Merge pull request #2309 from Dokploy/1778-railpack-frontend-version-is-hardcoded
1778 railpack frontend version is hardcoded
2025-08-03 02:22:40 -06:00
Mauricio Siu a138d12082 chore(tests): set railpackVersion to '0.2.2' in drop test configuration 2025-08-03 02:20:28 -06:00
Mauricio Siu a2405ddd84 chore(build): add cleanup step to remove builder container after Railpack build completion 2025-08-03 02:14:59 -06:00
Mauricio Siu e785ad5599 fix(build): set default railpackVersion to '0.2.2' and ensure cleanup in build process
- Updated the ShowBuildChooseForm component to default railpackVersion to '0.2.2' if not specified.
- Added cleanup step in the buildRailpack function to remove the builder container after execution.
- Refactored application router to include railpackVersion in the application schema.
2025-08-03 02:00:10 -06:00
Mauricio Siu cc6445a8ec feat(application): add railpackVersion field to application schema and update related components
- Introduced a new column `railpackVersion` in the application table with a default value of '0.2.2'.
- Updated the application form to include a field for `railpackVersion` when the build type is set to railpack.
- Adjusted the build process to utilize the specified `railpackVersion` dynamically.
- Enhanced validation schema to accommodate the new field.
2025-08-03 01:54:07 -06:00
Mauricio Siu b8f27d7b76 Merge pull request #2306 from Dokploy/2170-docker-compose-volume-mount-causes-oci-runtime-exec-failed-unless-container-is-force-recreated
refactor(docker): update docker exec command to set working directory…
2025-08-03 01:48:22 -06:00
Mauricio Siu 32f61b5e9b refactor(docker): update docker exec command to set working directory for terminal sessions 2025-08-03 01:47:31 -06:00
Mauricio Siu e2d6b5eb8a Merge pull request #2305 from Dokploy/2249-traefik-doesnt-reload-when-installed-as-a-docker-service
refactor(traefik): update Traefik initialization to support standalon…
2025-08-03 01:21:45 -06:00
Mauricio Siu 7413c9484a refactor(settings): remove unused getTraefikPorts function to streamline settings router 2025-08-03 01:19:14 -06:00
Mauricio Siu 607c505c4b refactor(traefik): update Traefik initialization to support standalone and service modes, enhance port handling with protocol specification 2025-08-03 01:18:18 -06:00
Mauricio Siu 42629e83a1 Merge pull request #2304 from Dokploy/1681-application-automated-build-fails-due-connection-http-basic-access-denied
refactor(gitea, gitlab): remove unused parameters and fetch entities …
2025-08-02 21:24:51 -06:00
Mauricio Siu b9f18cddf7 refactor(gitlab): reorder token refresh and provider fetching for improved clarity 2025-08-02 21:24:33 -06:00
Mauricio Siu 2790895642 refactor(gitea, gitlab): remove unused parameters and fetch entities by ID 2025-08-02 21:20:50 -06:00
Mauricio Siu c21c88d89f chore(package): bump version from v0.24.5 to v0.24.6 2025-08-02 19:37:10 -06:00
Mauricio Siu bf6b9c6893 Merge pull request #2302 from Dokploy/2281-url-rewrite-via-domain-routing-not-functioning
refactor(domain): enhance middleware handling for Traefik routers and…
2025-08-02 19:32:33 -06:00
Mauricio Siu e08fe1dbea test(labels): add comprehensive tests for middleware handling in createDomainLabels function 2025-08-02 19:31:21 -06:00
Mauricio Siu 0b9eaac390 refactor(domain): enhance middleware handling for Traefik routers and improve path validation 2025-08-02 19:24:11 -06:00
Mauricio Siu 5ed49a5ca1 Merge pull request #2301 from Dokploy/2074-keep-latest-is-not-respected
fix(backups): change backup file extension from .dump.gz to .sql.gz f…
2025-08-02 18:41:11 -06:00
Mauricio Siu 1300a6242c fix(backups): change backup file extension from .dump.gz to .sql.gz for consistency 2025-08-02 18:40:40 -06:00
Mauricio Siu 201f07c084 fix(volumes): adjust layout for volume display and improve conditional rendering 2025-08-02 16:13:51 -06:00
Mauricio Siu c5161f1612 Merge pull request #2300 from Dokploy/2259-file-mounts-not-updating
feat(mount): refactor updateMount logic and add updateFileMount funct…
2025-08-02 16:08:15 -06:00
Mauricio Siu 0755de03c2 feat(mount): refactor updateMount logic and add updateFileMount function for handling file mounts 2025-08-02 16:06:46 -06:00
Mauricio Siu f376ea5fec Merge pull request #2297 from Dokploy/feat/add-custom-service-field-domains-compose
feat(dashboard): add manual input for service name in domains for docker compose
2025-08-02 13:21:01 -06:00
Mauricio Siu 346eb24926 feat(dashboard): add manual input option for service name selection in domain handling 2025-08-02 13:20:00 -06:00
Mauricio Siu fe45c69939 Merge pull request #2296 from Dokploy/2291-mise-is-trying-to-verify-the-gpg-signature-of-the-nodejs-binary-for-node22180-but-it-cant-find-the-public-key-used-to-verify-the-signature
chore: update Railpack version to 0.2.2 in Dockerfile and related scr…
2025-08-02 13:14:58 -06:00
Mauricio Siu 39d46a51b3 chore: update Railpack version to 0.2.2 in Dockerfile and related scripts 2025-08-02 13:08:46 -06:00
Mauricio Siu 3e193590cc Merge pull request #2295 from JamBalaya56562/readme
docs: polish `README.md`
2025-08-02 12:50:26 -06:00
Mauricio Siu c157a353f3 Merge pull request #2279 from A-D-E/fix/gitlab-branches-pagination
The getGitlabBranches function was only returning the first 20 branches
2025-08-02 12:48:32 -06:00
Mauricio Siu 2a14ae0c7f Merge branch 'canary' into fix/gitlab-branches-pagination 2025-08-02 12:44:14 -06:00
JamBalaya56562 144c74e7f7 docs: polish README.md 2025-08-02 20:38:28 +09:00
Mauricio Siu 1d4d766f3a Merge pull request #2229 from danielepintore/canary
feat(dashboard): generate user fallback avatar using user email and allow user to choose default avatar
2025-08-02 00:34:16 -06:00
Mauricio Siu 8532cba638 Merge pull request #2255 from Marukome0743/lint-layouts
refactor: lint apps/components/layouts files
2025-08-02 00:27:09 -06:00
Mauricio Siu fdb4b176cb Merge pull request #2254 from Marukome0743/lint-test
refactor: lint apps/docker/__test__ files
2025-08-02 00:27:00 -06:00
Mauricio Siu f2b214f8f0 Merge pull request #2266 from rainwashed/canary
fix: github app creation name conflicting with already existing Dokploy names
2025-08-02 00:23:15 -06:00
autofix-ci[bot] 0bcc59f90f [autofix.ci] apply automated fixes 2025-08-02 06:19:09 +00:00
Mauricio Siu 7ae4bf3215 Merge pull request #2290 from Dokploy/2277-two-copies-of-the-same-volume-backup-are-uploaded-to-s3
refactor(backup): consolidate utility imports and add local backup cl…
2025-08-01 01:28:17 -06:00
Mauricio Siu 0f5cf37757 refactor(backup): consolidate utility imports and add local backup cleanup after S3 upload 2025-08-01 01:27:35 -06:00
Mauricio Siu a7bde655da Merge pull request #2289 from Dokploy/2282-custom-compose-path-not-being-used-when-manually-restarting-service
refactor(compose): reorganize imports and simplify command execution …
2025-08-01 01:16:33 -06:00
Mauricio Siu 295b6df5e1 refactor(compose): reorganize imports and simplify command execution for starting Docker Compose 2025-08-01 01:15:29 -06:00
Mauricio Siu b5b63eae4f Merge pull request #2288 from Dokploy/git-fetch-origin-git-checkout-2263-swarm-containers-no-data-found
refactor(application): update application handling to support multipl…
2025-08-01 00:35:32 -06:00
Mauricio Siu 794e03460f refactor(application): update application handling to support multiple app names and improve data structure 2025-08-01 00:34:57 -06:00
A-D-E e8f36f8ba5 The getGitlabBranches function was only returning the first 20 branches
due to GitLab's default API pagination limit. This prevented users from
accessing branches in repositories with more than 20 branches.

Changes:
- Add pagination loop to fetch all branches across multiple pages
- Set per_page to 100 (GitLab's maximum) for efficiency
- Add safety check using x-total header to prevent unnecessary requests
- Follow the same pagination pattern as validateGitlabProvider function

Fixes issue where branch selection was limited to first 20 branches
in repositories with many branches.
2025-07-30 15:17:14 +02:00
Daniele Pintore f9210d3165 lint: formatted changes using biome 2025-07-28 23:39:06 +02:00
rainwashed 9bc6411c98 fix: github app creation name conflicting with already existing Dokploy-Time names
appended a 5-char random string to the name creation as to prevent
conflicts with other existing Dokploy GitHub apps.
2025-07-28 17:18:30 -04:00
Daniele Pintore f8261b5364 feat(dashboard): use username instead of email for the generation of the
fallback avatar image
2025-07-28 19:20:05 +02:00
Daniele Pintore 30c2c7afb0 feat(dashboard): generate user fallback avatar using user email. Allow
user to select the default avatar.
2025-07-28 16:17:52 +02:00
Marukome0743 f26c1c0da6 refactor: lint apps/docker/__test__ files 2025-07-28 20:32:08 +09:00
Marukome0743 d02976476a refactor: lint apps/components/layouts files 2025-07-28 19:56:44 +09:00
Mauricio Siu 17e9154887 Merge pull request #2257 from Dokploy/fix/send-build-error-on-remote-servers
Fix/send build error on remote servers
2025-07-28 01:52:57 -06:00
Mauricio Siu 2442494096 fix(application): simplify error message handling in deployment notifications 2025-07-28 01:51:21 -06:00
Mauricio Siu bac2afb423 refactor(application): exclude appName from updateApplication data to streamline database updates 2025-07-28 01:50:58 -06:00
Mauricio Siu 4e9630e976 Merge pull request #2256 from Dokploy/feat/enhancements-cloud-version-ui
feat(dashboard): enhance application and database forms with tooltips…
2025-07-28 01:50:26 -06:00
Mauricio Siu 558f6aecae fix(application): improve error handling and notification messages during deployment 2025-07-28 01:48:33 -06:00
Mauricio Siu c3e2b0d0f1 feat(dashboard): enhance application and database forms with tooltips for better user guidance 2025-07-28 01:12:43 -06:00
Mauricio Siu 11d584316a chore(package): bump version to v0.24.5 2025-07-28 00:57:44 -06:00
Mauricio Siu f78dc555b2 Merge pull request #2244 from jhon2c/feat/improve-server-ux
feat(ux): Improve UX Based on Community Feedback
2025-07-27 23:21:24 -06:00
Mauricio Siu 5812b12a59 Merge pull request #2236 from masesisaac/canary
fix(dashboard): Update app security view to hide password
2025-07-27 23:16:07 -06:00
Mauricio Siu 7301d15e8f Merge pull request #2230 from amustapha/patch-1
fix: wrap user prompt in ai modal to prevent text stretch
2025-07-27 23:15:01 -06:00
Mauricio Siu f79796a6c8 Merge pull request #2188 from Marukome0743/vscode
chore: add biome settings for vscode editor
2025-07-27 23:14:33 -06:00
Mauricio Siu 4122b37abd Merge pull request #2250 from Dokploy/feat/add-name-field-to-profile
feat(profile): add optional name field to user profile form and schema
2025-07-27 23:13:26 -06:00
Mauricio Siu 79e9593663 feat(profile): add optional name field to user profile form and schema 2025-07-27 23:13:06 -06:00
masesisaac def3fa0030 fix(security): change password input type to 'password' 2025-07-28 04:58:43 +03:00
autofix-ci[bot] d561068bcd [autofix.ci] apply automated fixes 2025-07-26 17:26:20 +00:00
Jhon 212c1b2d5f feat(dashboard): show "Action Required" badge for incomplete Git provider setup 2025-07-26 14:18:26 -03:00
Jhon d3a54172b5 feat(ux): add conditional server selection functionality to application forms 2025-07-26 13:53:28 -03:00
masesisaac cda33eb291 refactor(dashboard): reorder imports in show-security.tsx for consistency 2025-07-24 17:45:26 +03:00
masesisaac c178234e53 fix(dashboard): hide basic auth password by default 2025-07-24 17:41:51 +03:00
Abdulhakeem Adetunji Mustapha 329db1fd1a fix: wrap user prompt in ai modal to prevent text stretch 2025-07-23 19:30:47 +01:00
Marukome0743 6efbf030a7 chore: add biome settings for vscode editor 2025-07-23 08:49:59 +09:00
Mauricio Siu b95dfed8fc chore(package): bump version to v0.24.4 2025-07-20 20:06:47 -06:00
Mauricio Siu 7fe3418d55 Merge pull request #2218 from Dokploy/2179-reloading-traefik-on-the-remote-server-will-cause-traefik-on-the-instance-to-change-accordingly
fix(traefik): remove duplicate file write operation in writeTraefikCo…
2025-07-20 20:05:48 -06:00
Mauricio Siu 288d86c73b fix(traefik): remove duplicate file write operation in writeTraefikConfigInPath function 2025-07-20 20:05:30 -06:00
Mauricio Siu ffd5ccd386 Merge pull request #2202 from gentslava/feat/traefik-config
feat(config): Traefik
2025-07-20 19:45:53 -06:00
Mauricio Siu 98ddd096e5 Update packages/server/src/setup/traefik-setup.ts 2025-07-20 19:45:41 -06:00
Mauricio Siu da6cc9fe72 Merge pull request #2190 from Marukome0743/format
chore: version up format.yml actions
2025-07-20 19:44:20 -06:00
Mauricio Siu 22d0af269e Merge pull request #2200 from Marukome0743/server
refactor: lint and sort imports on dokploy/server
2025-07-20 19:42:15 -06:00
Mauricio Siu f0fdc46de5 Merge pull request #2187 from Marukome0743/v2
chore: upgrade to Biome v2
2025-07-20 19:41:49 -06:00
Mauricio Siu 9aea24115d Merge pull request #2199 from Marukome0743/lint
refactor: lint and sort import on dokploy application
2025-07-20 19:41:02 -06:00
Mauricio Siu a9ee6c2393 Merge pull request #2194 from Marukome0743/pnpm
chore(package): version up pnpm to v9.12.0
2025-07-20 19:40:09 -06:00
Mauricio Siu 349717044c Merge pull request #2196 from Marukome0743/dispatch
ci: remove custom branch and add workflow_dispatch event
2025-07-20 19:37:27 -06:00
Mauricio Siu f94f32695f Merge pull request #2195 from Marukome0743/monitoring
chore: remove `apps/monitoring` from `pnpm-workspace.yaml`
2025-07-20 19:37:07 -06:00
Mauricio Siu 37b78ea09c Merge pull request #2217 from Dokploy/2201-daily-docker-cleanup-not-working-on-remote-server
fix(dashboard): update Docker cleanup toggle logic to prioritize serv…
2025-07-20 19:01:46 -06:00
Mauricio Siu 9b89b4631f fix(dashboard): update Docker cleanup toggle logic to prioritize server settings 2025-07-20 19:01:20 -06:00
Mauricio Siu 7100095f2b Merge pull request #2216 from Dokploy/2209-update-s3-destination-form-loses-its-state-when-current-tab-loses-its-focus
fix(dashboard): disable refetch on window focus for destination handling
2025-07-20 18:57:33 -06:00
Mauricio Siu a36ab65aa6 fix(dashboard): disable refetch on window focus for destination handling 2025-07-20 18:56:35 -06:00
Mauricio Siu bf81ba20ff Merge pull request #2215 from Dokploy/2197-git-provider-api-undefined_value-error
refactor(auth): simplify user session structure in validateRequest fu…
2025-07-20 18:55:16 -06:00
Mauricio Siu 658a4a9b99 refactor(auth): simplify user session structure in validateRequest function
- Changed user object in mockSession to only include userId, removing email and name for a more streamlined session representation.
2025-07-20 18:54:57 -06:00
Mauricio Siu 47cb096cf3 Merge pull request #2214 from Dokploy/2203-identical-webhook-redeploy-url-after-duplicating-project
feat(project): add refreshToken to application and compose data retri…
2025-07-20 18:45:39 -06:00
Mauricio Siu f3856722da feat(project): add refreshToken to application and compose data retrieval
- Included refreshToken in the data returned from findApplicationById and findComposeById functions to enhance application state management.
2025-07-20 18:45:18 -06:00
Vyacheslav Scherbinin a67c3eb979 feat(conf): accessLog filePath 2025-07-16 16:46:47 +07:00
Vyacheslav Scherbinin aaa205f104 feat(conf): disable sendAnonymousUsage 2025-07-16 12:29:31 +07:00
Marukome0743 cadea7ff28 refactor: lint and sort imports on dokploy/server 2025-07-15 14:22:37 +09:00
Marukome0743 9ab937f726 refactor: lint dokploy application 2025-07-15 14:13:32 +09:00
Marukome0743 d0af517eb7 ci: remove custom branch and add workflow_dispatch event 2025-07-14 19:03:41 +09:00
Marukome0743 66bdf9bf0a chore: remove apps/monitoring from pnpm-workspace.yaml 2025-07-14 18:24:26 +09:00
Marukome0743 d4a3af475a chore(package): version up pnpm to v9.12.0 2025-07-14 15:58:20 +09:00
autofix-ci[bot] e92a8d7c98 [autofix.ci] apply automated fixes 2025-07-14 15:30:24 +09:00
Marukome0743 c4fec8cee5 chore: upgrade to Biome v2 2025-07-14 15:30:23 +09:00
Marukome0743 55f75bce53 chore: version up format.yml actions 2025-07-14 15:30:06 +09:00
Mauricio Siu fdc524d79d fix(ui): adjust layout in UpdateServer component
- Removed unnecessary padding from DialogContent for a cleaner appearance.
- Added margin-top to the button container for improved spacing.
2025-07-13 23:37:05 -06:00
Mauricio Siu 93d6662466 docs(preview): update collaborator permission description in preview settings 2025-07-13 23:26:41 -06:00
Mauricio Siu 1977235d31 Merge pull request #2192 from Dokploy/fix/preview-deployments-public-repos
feat(preview): add collaborator permission requirement for preview de…
2025-07-13 23:20:51 -06:00
Mauricio Siu 1dd713a1d1 fix(deploy): change preview deployment limit check to be exclusive 2025-07-13 23:20:23 -06:00
Mauricio Siu 18b65f28f2 chore(package): bump version to v0.24.3 and comment out unused trustedOrigins function in auth.ts 2025-07-13 23:19:31 -06:00
Mauricio Siu 666db23b8e test: add previewRequireCollaboratorPermissions field to drop and traefik test cases 2025-07-13 23:17:32 -06:00
Mauricio Siu 2ca5321fdc feat(preview): add collaborator permission requirement for preview deployments
- Introduced a new boolean field `previewRequireCollaboratorPermissions` in the application schema to enforce permission checks for preview deployments.
- Updated the UI to include a toggle for this setting in the preview deployment settings.
- Enhanced GitHub deployment handler to validate PR authors against the required permissions, blocking unauthorized deployments and providing security notifications.
- Added SQL migration to update the database schema accordingly.
2025-07-13 23:12:09 -06:00
Mauricio Siu 3f3ff9670b chore(package): bump version to v0.24.2 2025-07-13 20:45:33 -06:00
Mauricio Siu 7fb902551e Merge pull request #2189 from jhon2c/fix/logs-overflow
fix(logs): Restore overflow classnames in logs components
2025-07-13 20:44:34 -06:00
Jhon a201b3f979 fix(ui): regression of overflow-y-auto class in non dialog related componentes 2025-07-13 21:28:50 -03:00
Jhon 01d78e50fc fix(logs): adds back overflow classnames 2025-07-13 21:09:12 -03:00
Mauricio Siu 6681ba7bbd Merge pull request #2185 from Dokploy/fix/make-monitoring-restart-automatically
feat(monitoring): add RestartPolicy configuration for server and web …
2025-07-13 13:18:09 -06:00
Mauricio Siu 0b71411c0e feat(monitoring): add RestartPolicy configuration for server and web monitoring setups 2025-07-13 13:17:48 -06:00
Mauricio Siu 19f7465910 chore(docker): activate pnpm 9.12.0 in all Dockerfiles 2025-07-13 13:11:19 -06:00
Mauricio Siu f33dd37571 Merge pull request #2184 from Dokploy/refactor/update-docker-base-images
chore(docker): update Node.js version to 20.16.0 in all Dockerfiles
2025-07-13 12:54:32 -06:00
Mauricio Siu a0031ed07f chore(docker): update Node.js version to 20.16.0 in all Dockerfiles 2025-07-13 12:03:01 -06:00
Mauricio Siu 2ca4e264c4 Merge pull request #2082 from Marukome0743/dependencies
chore: match dependencies with current ones in pnpm-lock.yaml
2025-07-13 12:01:12 -06:00
Mauricio Siu fa81d04fb3 Merge pull request #2164 from croatialu/fix/gitlab-deployments
fix(gitlab): Support dynamically generating clone URLs based on protocols
2025-07-13 11:55:19 -06:00
Mauricio Siu bd8745393b chore(package): bump version to v0.24.1 2025-07-13 11:55:04 -06:00
autofix-ci[bot] 691c83c256 [autofix.ci] apply automated fixes 2025-07-13 17:54:36 +00:00
Mauricio Siu 6bd85e9216 Merge pull request #2182 from jhon2c/fix/dialog-crash
fix(ui):  Fix Dialogs Infinite Render Loops and Command Component Conflicts
2025-07-13 11:53:11 -06:00
Jhon 79c29fa92d fix(typo): fixed typo on replace classname 2025-07-13 13:58:25 -03:00
autofix-ci[bot] 89f71fe889 [autofix.ci] apply automated fixes 2025-07-13 16:50:41 +00:00
Jhon bddafe294d fix(classname): removes leading blank space on classnames 2025-07-13 13:47:27 -03:00
Jhon 94829daf15 fix(ui): code formatting and DialogHeader improvements
- Apply consistent code formatting across dialog components
- Add bottom padding to DialogHeader for better visual separation
- Clean up DialogHeader usage in swarm settings (remove duplicate padding)
- Improve schedule dialog layout and add proper description
- Fix indentation and formatting inconsistencies

Final cleanup of dialog component formatting and spacing.
2025-07-13 13:35:26 -03:00
Jhon 2209d44ea5 fix(ui): update remaining dialog components with improved layouts
- Fix application import dialog positioning
- Update organization management dialog styling
- Ensure consistent DialogFooter behavior across all components

Completes the dialog layout improvements for better spacing and positioning.
2025-07-13 13:03:24 -03:00
Jhon b12c035527 fix(ui): improve DialogFooter layout in settings dialogs
- Update certificate management dialog footer styling
- Enhance destination settings dialog layout
- Improve notification settings dialog footer spacing
- Add responsive design improvements for server creation dialog

Ensures consistent footer behavior across settings panels.
2025-07-13 13:03:11 -03:00
Jhon baadba542f fix(ui): update DialogFooter styling in cluster management dialogs
- Add responsive layout and proper spacing to swarm settings footer
- Update registry dialog footer with improved flex layout
- Ensure proper button alignment on mobile and desktop
- Add sticky positioning for better UX in long forms
2025-07-13 13:01:36 -03:00
Jhon a8fc052cbf fix(ui): resolve dialog closing issues with Command components
- Replace custom overlay click handler with proper onInteractOutside
- Add detection for Command components to prevent unwanted closures
- Restore overlay visibility without click handler conflicts
- Separate DialogFooter from scrollable content for proper spacing
- Add border and padding to DialogFooter container for visual separation

Fixes dialogs closing unexpectedly when used inside Command menus.
2025-07-13 13:00:21 -03:00
Jhon fa5994bd47 fix(ui): remove max-h-screen and overflow-y-auto from remaining dialogs
Clean up any remaining dialog components with problematic CSS classes.
Complete removal of classes that interfere with new scroll handling system.
2025-07-13 12:17:05 -03:00
Jhon 96d0810607 fix(ui): remove max-h-screen and overflow-y-auto from project and database dialogs
Remove problematic CSS classes from:
- Project creation and management dialogs
- Database backup and restore dialogs
- Compose service management dialogs
- Template and AI generator dialogs

Ensures stable dialog behavior.
2025-07-13 12:16:51 -03:00
Jhon 2d382ea1be fix(ui): remove max-h-screen and overflow-y-auto from settings dialogs
Remove problematic CSS classes from system settings:
- Git provider configurations
- User management dialogs
- API key management
- Certificate management
- Notification settings
- Server management dialogs
- Profile and 2FA settings

Fixes render loops in admin panels.
2025-07-13 12:16:35 -03:00
Jhon d78974efc0 fix(ui): remove max-h-screen and overflow-y-auto from advanced settings dialogs
Remove problematic CSS classes from advanced application dialogs:
- Cluster and swarm settings
- Port configuration
- Security settings
- Traefik configuration
- Volume management
- Redirect configuration

Prevents tab hangs with overflow content.
2025-07-13 12:15:36 -03:00
Jhon 81040c899f fix(ui): remove max-h-screen and overflow-y-auto from application feature dialogs
Remove problematic CSS classes from:
- Domain management dialogs
- Preview deployment dialogs
- Schedule configuration dialogs
- Volume backup dialogs

Ensures proper scrolling without render loops.
2025-07-13 12:15:09 -03:00
Jhon c7344190b4 fix(ui): remove max-h-screen and overflow-y-auto from deployment dialogs
Remove problematic CSS classes from:
- Application deployment modals
- Docker logs modals
- Swarm application dialogs

Fixes infinite render loops with tall content.
2025-07-13 12:14:49 -03:00
Jhon 257c0eb106 fix(ui): remove max-h-screen and overflow-y-auto from service update dialogs
Remove problematic CSS classes that cause infinite render loops in:
- Application update dialog
- Database update dialogs (Redis, MariaDB, MongoDB, PostgreSQL, MySQL)
- Compose update dialog

These classes are now handled internally by the DialogContent component.
2025-07-13 12:14:36 -03:00
Jhon c03b9509c8 fix(ui): resolve dialog infinite render loops with tall content
- Force modal=false on all dialogs to prevent Radix UI render loops
- Add React context to share dialog state between components
- Implement custom overlay with proper click-to-close behavior
- Add body scroll lock tied to dialog open state (prevents stuck scroll)
- Create scrollable content wrapper with overscroll-contain
- Remove complex wheel event handlers that caused tab hangs
- Simplify dialog architecture for better maintainability
2025-07-13 11:36:10 -03:00
Mauricio Siu d87205c4dc chore: update README.md by removing outdated sponsor links and adjusting community backers section 2025-07-13 01:56:17 -06:00
Mauricio Siu 48aef798e4 Merge pull request #2176 from gentslava/fix/git-providers-layout
fix(ui): git providers overflow
2025-07-13 01:23:48 -06:00
Vyacheslav Scherbinin baa5cd5c58 fix(ui): available git providers layout 2025-07-12 14:05:44 +07:00
Vyacheslav Scherbinin 5aae36996e fix(ui): buttons grow 2025-07-12 13:52:53 +07:00
Vyacheslav Scherbinin ec8fa9fefe fix(ui): buttons wrap 2025-07-12 13:50:43 +07:00
Vyacheslav Scherbinin d959f59c2d fix(typo): double space 2025-07-12 13:36:28 +07:00
Mauricio Siu a1169795e4 Merge pull request #2163 from croatialu/fix/gitlab-url
fix: Add gitlabUrl calculation logic and update link references
2025-07-12 00:15:12 -06:00
Mauricio Siu 10af7925db Merge pull request #2156 from gentslava/fix/overflow-scroll
fix(ui): tabs overflow and Tailwind config
2025-07-11 22:58:41 -06:00
Mauricio Siu c64cdca2e8 Merge pull request #2174 from Dokploy/2147-container-name-exceeds-63-characters-when-cloning-multiple-projects
feat(project): update application name handling during duplication
2025-07-11 22:53:06 -06:00
Mauricio Siu a5b95d8cf3 feat(project): update application name handling during duplication
- Extracted and modified the application name by removing the suffix after the last hyphen when duplicating various application types (Postgres, MariaDB, Mongo, MySQL, Redis, Compose).
- Ensured consistent naming for duplicated applications across different database types.
2025-07-11 22:52:47 -06:00
Mauricio Siu 78b60f7d8a Merge pull request #2167 from croatialu/fix/traefik-config-editor-mask
fix: Optimize the code editor component, adjust the style and structu…
2025-07-11 22:29:23 -06:00
autofix-ci[bot] 58e6a14cd6 [autofix.ci] apply automated fixes 2025-07-12 04:28:55 +00:00
croatialu 0aac6da554 fix: Optimize the code editor component, adjust the style and structure to ensure the overlay is correctly rendered in the disabled state. 2025-07-11 14:19:58 +08:00
croatialu 978c4d85c5 fix(gitlab): Support dynamically generating clone URLs based on protocols 2025-07-11 13:33:46 +08:00
croatialu 70e08c96eb fix: Add gitlabUrl calculation logic and update link references
- Use the useMemo hook to calculate gitlabUrl in the SaveGitlabProvider component.
- Update link references to use the dynamically generated gitlabUrl, ensuring links correctly point to the corresponding GitLab repositories.
2025-07-11 11:55:16 +08:00
Vyacheslav Scherbinin 027853a361 fix(ui): change gap 2025-07-09 18:20:07 +07:00
Vyacheslav Scherbinin 43ebe4dc7c fix(config): the min- and max- variants are not supported with a screens configuration containing mixed units 2025-07-09 18:17:32 +07:00
Vyacheslav Scherbinin 0113ebe7da fix(ui): compose provider tabs layout 2025-07-09 14:20:17 +07:00
Vyacheslav Scherbinin c36b40aa29 fix(ui): application provider tabs layout 2025-07-09 14:20:07 +07:00
Vyacheslav Scherbinin caea934f88 fix(typo): double space 2025-07-09 14:16:02 +07:00
autofix-ci[bot] 9b2ea1cade [autofix.ci] apply automated fixes 2025-07-09 07:07:04 +00:00
Vyacheslav Scherbinin 3a82c4b27b fix(ui): compose tabs overflow 2025-07-09 13:53:14 +07:00
Vyacheslav Scherbinin 22a26e9873 fix(ui): application tabs overflow 2025-07-09 13:53:05 +07:00
Marukome0743 226a287ce7 chore: update package.json 2025-07-09 15:33:37 +09:00
Mauricio Siu 320b927aac Merge pull request #2152 from nktnet1/fix-ui-compose-tablist
fix(ui): adjust tablist item width for compose services
2025-07-08 21:47:16 -06:00
Khiet Tam Nguyen d799b460bd fix(ui): adjust tablist item width for compose services 2025-07-08 20:08:26 +10:00
240 changed files with 22396 additions and 1967 deletions
+21
View File
@@ -0,0 +1,21 @@
## What is this PR about?
Please describe in a short paragraph what this PR is about.
## Checklist
Before submitting this PR, please make sure that:
- [ ] You created a dedicated branch based on the `canary` branch.
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have tested this PR in your local instance.
## Issues related (if applicable)
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER`
Example: `closes #123`
## Screenshots (if applicable)
If you include a video or screenshot, would be awesome so we can see the changes in action.
-4
View File
@@ -19,17 +19,14 @@ jobs:
fetch-depth: 0
- name: Get version from package.json
id: package_version
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
- name: Get latest GitHub tag
id: latest_tag
run: |
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1)
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
echo $LATEST_TAG
- name: Compare versions
id: compare_versions
run: |
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
VERSION_CHANGED="true"
@@ -42,7 +39,6 @@ jobs:
echo "Latest tag: ${{ env.LATEST_TAG }}"
echo "Version changed: $VERSION_CHANGED"
- name: Check if a PR already exists
id: check_pr
run: |
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV
+2 -1
View File
@@ -2,7 +2,8 @@ name: Build Docker images
on:
push:
branches: ["canary", "main", "feat/monitoring"]
branches: [main, canary]
workflow_dispatch:
jobs:
build-and-push-cloud-image:
+2 -1
View File
@@ -2,7 +2,8 @@ name: Dokploy Docker Build
on:
push:
branches: [main, canary, "1061-custom-docker-service-hostname"]
branches: [main, canary]
workflow_dispatch:
env:
IMAGE_NAME: dokploy/dokploy
+3 -3
View File
@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup biomeJs
uses: biomejs/setup-biome@v2
- name: Run Biome formatter
run: biome format . --write
run: biome format --write
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}
+11 -4
View File
@@ -87,7 +87,8 @@ pnpm run dokploy:dev
Go to http://localhost:3000 to see the development server
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
> [!NOTE]
> This project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
## Build
@@ -117,10 +118,10 @@ In the case you lost your password, you can reset it using the following command
pnpm run reset-password
```
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
If you want to test the webhooks on development mode using localtunnel, make sure to install [`localtunnel`](https://localtunnel.app/)
```bash
bunx lt --port 3000
pnpm dlx localtunnel --port 3000
```
If you run into permission issues of docker run the following command
@@ -152,7 +153,7 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release.
- The `canary` branch is the source of truth and should always reflect the latest stable release.
- Create a new branch for each feature or bug fix.
- Make sure to add tests for your changes.
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
@@ -161,6 +162,12 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
- If your pull request fixes an open issue, please reference the issue in the pull request description.
- Once your pull request is merged, you will be automatically added as a contributor to the project.
**Important Considerations for Pull Requests:**
- **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`).
Thank you for your contribution!
## Templates
+3 -2
View File
@@ -1,8 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base
FROM node:20.16.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@9.12.0 --activate
FROM base AS build
COPY . /usr/src/app
@@ -57,7 +58,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& pnpm install -g tsx
# Install Railpack
ARG RAILPACK_VERSION=0.0.64
ARG RAILPACK_VERSION=0.2.2
RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks
+2 -1
View File
@@ -1,8 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base
FROM node:20.16.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@9.12.0 --activate
FROM base AS build
COPY . /usr/src/app
+2 -1
View File
@@ -1,8 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base
FROM node:20.16.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@9.12.0 --activate
FROM base AS build
COPY . /usr/src/app
+2 -1
View File
@@ -1,8 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base
FROM node:20.16.0-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@9.12.0 --activate
FROM base AS build
COPY . /usr/src/app
+9 -19
View File
@@ -1,6 +1,6 @@
<div align="center">
<a href="https://dokploy.com">
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." align="center" width="100%" />
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." width="100%" />
</a>
</br>
</br>
@@ -13,7 +13,7 @@
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features
## ✨ Features
Dokploy includes multiple features to make your life easier.
@@ -43,7 +43,7 @@ curl -sSL https://dokploy.com/install.sh | sh
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
## Sponsors
## ♥️ Sponsors
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
@@ -60,8 +60,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<div>
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
<a href="https://mandarin3d.com/?ref=dokploy"><img src=".github/sponsors/mandarin.png" alt="Mandarin" width="100"/></a>
<a href="https://lightnode.com/?ref=dokploy"><img src=".github/sponsors/light-node.webp" alt="Lightnode" width="300"/></a>
</div>
<!-- Premium Supporters 🥇 -->
@@ -89,25 +87,17 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Supporting Members 🥉
<div>
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://cloudblast.io/?ref=dokploy"><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
<a href="https://startupfa.me/?ref=dokploy"><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
<a href="https://itsdb-center.com?ref=dokploy"><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
<a href="https://openalternative.co/?ref=dokploy"><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
</div>
### Community Backers 🤝
<div>
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
</div>
#### Organizations:
[![Sponsors on Open Collective](https://opencollective.com/dokploy/organizations.svg?width=890)](https://opencollective.com/dokploy)
[Sponsors on Open Collective](https://opencollective.com/dokploy)
#### Individuals:
@@ -116,15 +106,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Contributors 🤝
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" alt="Contributors" />
</a>
## Video Tutorial
## 📺 Video Tutorial
<a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
</a>
## Contributing
## 🤝 Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
+12 -8
View File
@@ -10,24 +10,28 @@
},
"dependencies": {
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.12.1",
"@hono/node-server": "^1.14.3",
"@hono/zod-validator": "0.3.0",
"@nerimity/mimiqueue": "1.2.3",
"dotenv": "^16.3.1",
"hono": "^4.5.8",
"dotenv": "^16.4.5",
"hono": "^4.7.10",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"redis": "4.7.0",
"zod": "^3.23.4"
"zod": "^3.25.32"
},
"devDependencies": {
"@types/node": "^20.11.17",
"@types/node": "^20.17.51",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"tsx": "^4.7.1",
"typescript": "^5.4.2"
"tsx": "^4.16.2",
"typescript": "^5.8.3"
},
"packageManager": "pnpm@9.5.0"
"packageManager": "pnpm@9.12.0",
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
}
@@ -1,5 +1,5 @@
import { addSuffixToAllProperties } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllProperties } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToConfigsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllConfigs } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -108,4 +108,136 @@ describe("createDomainLabels", () => {
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
);
});
it("should add stripPath middleware when stripPath is enabled", async () => {
const stripPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
};
const labels = await createDomainLabels(appName, stripPathDomain, "web");
expect(labels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=stripprefix-test-app-1",
);
});
it("should add internalPath middleware when internalPath is set", async () => {
const internalPathDomain = {
...baseDomain,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(
appName,
internalPathDomain,
"web",
);
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Middleware definition should only appear in web entrypoint
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Both routers should reference the middleware
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=addprefix-test-app-1",
);
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
});
it("should combine HTTPS redirect with internalPath middleware in correct order", async () => {
const combinedDomain = {
...baseDomain,
https: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, combinedDomain, "web");
const websecureLabels = await createDomainLabels(
appName,
combinedDomain,
"websecure",
);
// Web entrypoint should have both middlewares with redirect first
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,addprefix-test-app-1",
);
// Websecure should only have the addprefix middleware
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
// Middleware definition should only appear once (in web)
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
});
it("should combine all middlewares in correct order", async () => {
const fullDomain = {
...baseDomain,
https: true,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, fullDomain, "web");
// Should have all middleware definitions (only in web)
expect(webLabels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Should have middlewares in correct order: redirect, stripprefix, addprefix
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,stripprefix-test-app-1,addprefix-test-app-1",
);
});
it("should not add middleware definitions for websecure entrypoint", async () => {
const internalPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Should not contain any middleware definitions
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// But should reference the middlewares
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
);
});
});
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNetworks } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,10 +1,10 @@
import { generateRandomHash } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllNetworks,
addSuffixToNetworksRoot,
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToSecretsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,5 +1,5 @@
import { addSuffixToAllSecrets } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllSecrets } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,8 +1,8 @@
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllServiceNames,
addSuffixToServiceNames,
} from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,9 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllVolumes,
addSuffixToVolumesRoot,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToVolumesInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -1,5 +1,5 @@
import { addSuffixToAllVolumes } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllVolumes } from "@dokploy/server";
import { load } from "js-yaml";
import { expect, test } from "vitest";
+1 -1
View File
@@ -1,5 +1,5 @@
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
import { describe, expect, it } from "vitest";
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = {
@@ -1,12 +1,12 @@
import fs from "node:fs/promises";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server";
import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
@@ -25,10 +25,12 @@ if (typeof window === "undefined") {
}
const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
applicationId: "",
herokuVersion: "",
giteaBranch: "",
giteaBuildPath: "",
previewRequireCollaboratorPermissions: false,
giteaId: "",
giteaOwner: "",
giteaRepository: "",
@@ -141,7 +143,7 @@ describe("unzipDrop using real zip files", () => {
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();
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 });
@@ -1,5 +1,6 @@
import { parseRawConfig, processLogs } from "@dokploy/server";
import { describe, expect, it } from "vitest";
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
describe("processLogs", () => {
@@ -1,10 +1,9 @@
import type { Domain } from "@dokploy/server";
import type { Redirect } from "@dokploy/server";
import type { ApplicationNested } from "@dokploy/server";
import type { ApplicationNested, Domain, Redirect } from "@dokploy/server";
import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
rollbackActive: false,
applicationId: "",
herokuVersion: "",
@@ -18,6 +17,7 @@ const baseApp: ApplicationNested = {
appName: "",
autoDeploy: true,
enableSubmodules: false,
previewRequireCollaboratorPermissions: false,
serverId: "",
branch: null,
dockerBuildStage: "",
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Settings } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
@@ -26,12 +32,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Settings } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const HealthCheckSwarmSchema = z
.object({
@@ -181,21 +181,38 @@ const addSwarmSettings = z.object({
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
interface Props {
applicationId: string;
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
}
export const AddSwarmSettings = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{
enabled: !!applicationId,
},
);
export const AddSwarmSettings = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { mutateAsync, isError, error, isLoading } =
api.application.update.useMutation();
const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isError, error, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddSwarmSettings>({
defaultValues: {
@@ -244,7 +261,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
const onSubmit = async (data: AddSwarmSettings) => {
await mutateAsync({
applicationId,
applicationId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
healthCheckSwarm: data.healthCheckSwarm,
restartPolicySwarm: data.restartPolicySwarm,
placementSwarm: data.placementSwarm,
@@ -270,18 +292,18 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
Swarm Settings
</Button>
</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-5xl p-0">
<DialogHeader className="p-6">
<DialogContent className="sm:max-w-5xl">
<DialogHeader>
<DialogTitle>Swarm Settings</DialogTitle>
<DialogDescription>
Update certain settings using a json object.
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="px-4">
<div>
<AlertBlock type="info">
Changing settings such as placements may cause the logs/monitoring
to be unavailable.
Changing settings such as placements may cause the logs/monitoring,
backups and other features to be unavailable.
</AlertBlock>
</div>
@@ -289,13 +311,13 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
<form
id="hook-form-add-permissions"
onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative"
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative mt-4"
>
<FormField
control={form.control}
name="healthCheckSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormItem className="relative ">
<FormLabel>Health Check</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -351,7 +373,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="restartPolicySwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
<FormItem className="relative ">
<FormLabel>Restart Policy</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -405,7 +427,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="placementSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormItem className="relative ">
<FormLabel>Placement</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -471,7 +493,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="updateConfigSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
<FormItem className="relative ">
<FormLabel>Update Config</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -529,7 +551,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="rollbackConfigSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormItem className="relative ">
<FormLabel>Rollback Config</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -587,7 +609,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="modeSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
<FormItem className="relative ">
<FormLabel>Mode</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -650,7 +672,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="networkSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormItem className="relative ">
<FormLabel>Network</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -709,7 +731,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="labelsSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
<FormItem className="relative ">
<FormLabel>Labels</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -753,7 +775,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
)}
/>
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border p-2 ">
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
<Button
isLoading={isLoading}
form="hook-form-add-permissions"
@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
@@ -26,43 +33,57 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AddSwarmSettings } from "./modify-swarm-settings";
interface Props {
applicationId: string;
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
}
const AddRedirectchema = z.object({
replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string(),
registryId: z.string().optional(),
});
type AddCommand = z.infer<typeof AddRedirectchema>;
export const ShowClusterSettings = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
export const ShowClusterSettings = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { data: registries } = api.registry.all.useQuery();
const utils = api.useUtils();
const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isLoading } = api.application.update.useMutation();
const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddCommand>({
defaultValues: {
registryId: data?.registryId || "",
...(type === "application" && data && "registryId" in data
? {
registryId: data?.registryId || "",
}
: {}),
replicas: data?.replicas || 1,
},
resolver: zodResolver(AddRedirectchema),
@@ -71,7 +92,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
useEffect(() => {
if (data?.command) {
form.reset({
registryId: data?.registryId || "",
...(type === "application" && data && "registryId" in data
? {
registryId: data?.registryId || "",
}
: {}),
replicas: data?.replicas || 1,
});
}
@@ -79,18 +104,25 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
const onSubmit = async (data: AddCommand) => {
await mutateAsync({
applicationId,
registryId:
data?.registryId === "none" || !data?.registryId
? null
: data?.registryId,
applicationId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
...(type === "application"
? {
registryId:
data?.registryId === "none" || !data?.registryId
? null
: data?.registryId,
}
: {}),
replicas: data?.replicas,
})
.then(async () => {
toast.success("Command Updated");
await utils.application.one.invalidate({
applicationId,
});
await refetch();
})
.catch(() => {
toast.error("Error updating the command");
@@ -103,10 +135,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
<div>
<CardTitle className="text-xl">Cluster Settings</CardTitle>
<CardDescription>
Add the registry and the replicas of the application
Modify swarm settings for the service.
</CardDescription>
</div>
<AddSwarmSettings applicationId={applicationId} />
<AddSwarmSettings id={id} type={type} />
</CardHeader>
<CardContent className="flex flex-col gap-4">
<AlertBlock type="info">
@@ -144,58 +176,62 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
/>
</div>
{registries && registries?.length === 0 ? (
<div className="pt-10">
<div className="flex flex-col items-center gap-3">
<Server className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To use a cluster feature, you need to configure at least a
registry first. Please, go to{" "}
<Link
href="/dashboard/settings/cluster"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
</div>
) : (
{type === "application" && (
<>
<FormField
control={form.control}
name="registryId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a registry</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a registry" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{registries?.map((registry) => (
<SelectItem
key={registry.registryId}
value={registry.registryId}
>
{registry.registryName}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
<SelectLabel>
Registries ({registries?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
{registries && registries?.length === 0 ? (
<div className="pt-10">
<div className="flex flex-col items-center gap-3">
<Server className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To use a cluster feature, you need to configure at least
a registry first. Please, go to{" "}
<Link
href="/dashboard/settings/cluster"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
</div>
) : (
<>
<FormField
control={form.control}
name="registryId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a registry</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a registry" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{registries?.map((registry) => (
<SelectItem
key={registry.registryId}
value={registry.registryId}
>
{registry.registryName}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
<SelectLabel>
Registries ({registries?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
</>
)}
</>
)}
@@ -185,7 +185,7 @@ export const ShowImport = ({ composeId }: Props) => {
</Button>
</div>
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogContent className="max-h-[80vh] max-w-[50vw] overflow-y-auto">
<DialogContent className="max-w-[50vw]">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">
Template Information
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
@@ -26,12 +32,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535),
@@ -80,6 +80,11 @@ export const HandlePorts = ({
resolver: zodResolver(AddPortSchema),
});
const publishMode = useWatch({
control: form.control,
name: "publishMode",
});
useEffect(() => {
form.reset({
publishedPort: data?.publishedPort ?? 0,
@@ -124,7 +129,7 @@ export const HandlePorts = ({
<Button>{children}</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Ports</DialogTitle>
<DialogDescription>
@@ -253,6 +258,16 @@ export const HandlePorts = ({
</div>
</form>
{publishMode === "host" && (
<AlertBlock type="warning" className="mt-4">
<strong>Host Mode Limitation:</strong> When using Host publish
mode, Docker Swarm has limitations that prevent proper container
updates during deployments. Old containers may not be replaced
automatically. Consider using Ingress mode instead, or be prepared
to manually stop/start the application after deployments.
</AlertBlock>
)}
<DialogFooter>
<Button
isLoading={isLoading}
@@ -179,7 +179,7 @@ export const HandleRedirect = ({
<Button>{children}</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Redirects</DialogTitle>
<DialogDescription>
@@ -114,7 +114,7 @@ export const HandleSecurity = ({
<Button>{children}</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Security</DialogTitle>
<DialogDescription>
@@ -151,7 +151,7 @@ export const HandleSecurity = ({
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="test" {...field} />
<Input placeholder="test" type="password" {...field} />
</FormControl>
<FormMessage />
@@ -7,6 +7,9 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { LockKeyhole, Trash2 } from "lucide-react";
import { toast } from "sonner";
@@ -58,19 +61,18 @@ export const ShowSecurity = ({ applicationId }: Props) => {
<div className="flex flex-col gap-6 ">
{data?.security.map((security) => (
<div key={security.securityId}>
<div className="flex w-full flex-col sm:flex-row justify-between sm:items-center gap-4 sm:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Username</span>
<span className="text-sm text-muted-foreground">
{security.username}
</span>
<div className="flex w-full flex-col md:flex-row justify-between md:items-center gap-4 md:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-2 flex-col gap-4 md:gap-8">
<div className="flex flex-col gap-2">
<Label>Username</Label>
<Input disabled value={security.username} />
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Password</span>
<span className="text-sm text-muted-foreground">
{security.password}
</span>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<ToggleVisibilityInput
value={security.password}
disabled
/>
</div>
</div>
<div className="flex flex-row gap-2">
@@ -122,7 +122,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
<DialogTrigger asChild>
<Button isLoading={isLoading}>Modify</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Update traefik config</DialogTitle>
<DialogDescription>Update the traefik config</DialogDescription>
@@ -151,7 +151,7 @@ export const AddVolumes = ({
<DialogTrigger className="" asChild>
<Button>{children}</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Volumes / Mounts</DialogTitle>
</DialogHeader>
@@ -1,3 +1,5 @@
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
@@ -9,11 +11,10 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import type { ServiceType } from "../show-resources";
import { AddVolumes } from "./add-volumes";
import { UpdateVolume } from "./update-volume";
interface Props {
id: string;
type: ServiceType | "compose";
@@ -80,7 +81,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
>
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground">
@@ -112,21 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
</span>
</div>
)}
{mount.type === "file" ? (
{mount.type === "file" && (
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</span>
</div>
) : (
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
</div>
<div className="flex flex-row gap-1">
<UpdateVolume
@@ -186,7 +186,7 @@ export const UpdateVolume = ({
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Update</DialogTitle>
<DialogDescription>Update the mount</DialogDescription>
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -15,12 +21,6 @@ import {
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
export enum BuildType {
dockerfile = "dockerfile",
@@ -65,6 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}),
z.object({
buildType: z.literal(BuildType.railpack),
railpackVersion: z.string().nullable().default("0.2.2"),
}),
z.object({
buildType: z.literal(BuildType.static),
@@ -86,6 +87,7 @@ interface ApplicationData {
herokuVersion?: string | null;
publishDirectory?: string | null;
isStaticSpa?: boolean | null;
railpackVersion?: string | null | undefined;
}
function isValidBuildType(value: string): value is BuildType {
@@ -123,6 +125,7 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.railpack:
return {
buildType: BuildType.railpack,
railpackVersion: data.railpackVersion || null,
};
default: {
const buildType = data.buildType as BuildType;
@@ -181,6 +184,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
: null,
isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null,
railpackVersion:
data.buildType === BuildType.railpack
? data.railpackVersion || "0.2.2"
: null,
})
.then(async () => {
toast.success("Build type saved");
@@ -395,6 +402,25 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)}
/>
)}
{buildType === BuildType.railpack && (
<FormField
control={form.control}
name="railpackVersion"
render={({ field }) => (
<FormItem>
<FormLabel>Railpack Version</FormLabel>
<FormControl>
<Input
placeholder="Railpack Version"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save
@@ -124,7 +124,7 @@ export const ShowDeployment = ({
}
}}
>
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
<DialogContent className={"sm:max-w-5xl"}>
<DialogHeader>
<DialogTitle>Deployment</DialogTitle>
<DialogDescription className="flex items-center gap-2">
@@ -50,7 +50,7 @@ export const ShowDeploymentsModal = ({
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl p-0">
<DialogContent className="sm:max-w-5xl p-0">
<ShowDeployments
id={id}
type={type}
@@ -1,4 +1,5 @@
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -10,14 +11,13 @@ import {
CardTitle,
} from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api";
import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } from "lucide-react";
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { DialogAction } from "@/components/shared/dialog-action";
import { toast } from "sonner";
interface Props {
id: string;
@@ -33,7 +33,7 @@ export const DnsHelperModal = ({ domain, serverIp }: Props) => {
<HelpCircle className="size-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Server className="size-5" />
@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
@@ -34,14 +41,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import z from "zod";
export type CacheType = "fetch" | "cache";
@@ -123,6 +122,7 @@ interface Props {
export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache");
const [isManualInput, setIsManualInput] = useState(false);
const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery(
@@ -292,7 +292,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
@@ -325,46 +325,126 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<FormItem className="w-full">
<FormLabel>Service Name</FormLabel>
<div className="flex gap-2">
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
{isManualInput ? (
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
<Input
placeholder="Enter service name manually"
{...field}
className="w-full"
/>
</FormControl>
) : (
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent>
{services?.map((service, index) => (
<SelectItem
value={service}
key={`${service}-${index}`}
>
{service}
<SelectContent>
{services?.map((service, index) => (
<SelectItem
value={service}
key={`${service}-${index}`}
>
{service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem>
</SelectContent>
</Select>
</SelectContent>
</Select>
)}
{!isManualInput && (
<>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and
load the services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services
from the last deployment/fetch from
the repository
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
setIsManualInput(!isManualInput);
if (!isManualInput) {
field.onChange("");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
{isManualInput ? (
<RefreshCw className="size-4 text-muted-foreground" />
) : (
<span className="text-xs text-muted-foreground">
Manual
</span>
)}
</Button>
</TooltipTrigger>
<TooltipContent
@@ -373,40 +453,9 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and load
the services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services from
the last deployment/fetch from the
repository
{isManualInput
? "Switch to service selection"
: "Enter service name manually"}
</p>
</TooltipContent>
</Tooltip>
@@ -43,7 +43,7 @@ import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -96,6 +96,16 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
const repository = form.watch("repository");
const gitlabId = form.watch("gitlabId");
const gitlabUrl = useMemo(() => {
const url = gitlabProviders?.find(
(provider) => provider.gitlabId === gitlabId,
)?.gitlabUrl;
const gitlabUrl = url?.replace(/\/$/, "");
return gitlabUrl || "https://gitlab.com";
}, [gitlabId, gitlabProviders]);
const {
data: repositories,
isLoading: isLoadingRepositories,
@@ -224,7 +234,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && (
<Link
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
href={`${gitlabUrl}/${field.value.owner}/${field.value.repo}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
@@ -153,8 +153,8 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<div className="flex flex-row items-center justify-between w-full overflow-auto">
<TabsList className="flex gap-4 justify-start bg-transparent">
<TabsTrigger
value="github"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
@@ -138,7 +138,7 @@ export const AddPreviewDomain = ({
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
@@ -46,6 +46,7 @@ const schema = z
previewPath: z.string(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
previewCustomCertResolver: z.string().optional(),
previewRequireCollaboratorPermissions: z.boolean(),
})
.superRefine((input, ctx) => {
if (
@@ -83,6 +84,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewHttps: false,
previewPath: "/",
previewCertificateType: "none",
previewRequireCollaboratorPermissions: true,
},
resolver: zodResolver(schema),
});
@@ -105,6 +107,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: data.previewPath || "/",
previewCertificateType: data.previewCertificateType || "none",
previewCustomCertResolver: data.previewCustomCertResolver || "",
previewRequireCollaboratorPermissions:
data.previewRequireCollaboratorPermissions || true,
});
}
}, [data]);
@@ -121,6 +125,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: formData.previewPath,
previewCertificateType: formData.previewCertificateType,
previewCustomCertResolver: formData.previewCustomCertResolver,
previewRequireCollaboratorPermissions:
formData.previewRequireCollaboratorPermissions,
})
.then(() => {
toast.success("Preview Deployments settings updated");
@@ -138,7 +144,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
Configure
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
<DialogContent className="sm:max-w-5xl w-full">
<DialogHeader>
<DialogTitle>Preview Deployment Settings</DialogTitle>
<DialogDescription>
@@ -312,6 +318,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<FormField
control={form.control}
name="previewRequireCollaboratorPermissions"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
<div className="space-y-0.5">
<FormLabel>
Require Collaborator Permissions
</FormLabel>
<FormDescription>
Require collaborator permissions to preview
deployments, valid roles are:
<ul>
<li>Admin</li>
<li>Maintain</li>
<li>Write</li>
</ul>
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="env"
@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -232,14 +233,17 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
</DialogTrigger>
<DialogContent
className={cn(
"max-h-screen overflow-y-auto",
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
? "max-h-[95vh] sm:max-w-2xl"
: " sm:max-w-lg",
? "sm:max-w-2xl"
: "sm:max-w-lg",
)}
>
<DialogHeader>
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
<DialogDescription>
{scheduleId ? "Manage" : "Create"} a schedule to run a task at a
specific time or interval.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
@@ -91,7 +91,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
return (
<div
key={schedule.scheduleId}
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
>
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
@@ -99,7 +99,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify Application</DialogTitle>
<DialogDescription>Update the application data</DialogDescription>
@@ -257,7 +257,7 @@ export const HandleVolumeBackups = ({
</DialogTrigger>
<DialogContent
className={cn(
"max-h-screen overflow-y-auto",
"overflow-y-auto",
volumeBackupType === "compose" || volumeBackupType === "application"
? "max-h-[95vh] sm:max-w-2xl"
: " sm:max-w-lg",
@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -42,9 +43,8 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { formatBytes } from "../../database/backups/restore-backup";
import { AlertBlock } from "@/components/shared/alert-block";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
interface Props {
id: string;
@@ -161,7 +161,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
Restore Volume Backup
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center">
<RotateCcw className="mr-2 size-4" />
@@ -23,8 +23,8 @@ import {
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { HandleVolumeBackups } from "./handle-volume-backups";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleVolumeBackups } from "./handle-volume-backups";
import { RestoreVolumeBackups } from "./restore-volume-backups";
interface Props {
@@ -113,7 +113,7 @@ export const ShowVolumeBackups = ({
return (
<div
key={volumeBackup.volumeBackupId}
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
>
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
@@ -126,7 +126,7 @@ export const DeleteService = ({ id, type }: Props) => {
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
@@ -1,3 +1,4 @@
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import {
BitbucketIcon,
GitIcon,
@@ -11,6 +12,7 @@ import { api } from "@/utils/api";
import { CodeIcon, GitBranch, Loader2 } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
@@ -18,8 +20,6 @@ import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import { toast } from "sonner";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
interface Props {
@@ -142,8 +142,8 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
setSab(e as TabState);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<div className="flex flex-row items-center justify-between w-full overflow-auto">
<TabsList className="flex gap-4 justify-start bg-transparent">
<TabsTrigger
value="github"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
@@ -1,3 +1,6 @@
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
@@ -10,9 +13,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
interface Props {
composeId: string;
@@ -52,7 +52,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview Compose
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-6xl max-h-[50rem] overflow-y-auto">
<DialogContent className="sm:max-w-6xl max-h-[50rem]">
<DialogHeader>
<DialogTitle>Converted Compose</DialogTitle>
<DialogDescription>
@@ -79,7 +79,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
</div>
) : (
<>
<div className="flex flex-row gap-2 justify-end">
<div className="flex flex-row gap-2 justify-end my-4">
<Button
variant="secondary"
isLoading={isLoading}
@@ -23,7 +23,7 @@ export const ShowUtilities = ({ composeId }: Props) => {
<DialogTrigger asChild>
<Button variant="ghost">Show Utilities</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
<DialogContent className="sm:max-w-5xl">
<DialogHeader>
<DialogTitle>Utilities </DialogTitle>
<DialogDescription>Modify the application data</DialogDescription>
@@ -99,7 +99,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify Compose</DialogTitle>
<DialogDescription>Update the compose data</DialogDescription>
@@ -329,7 +329,7 @@ export const HandleBackup = ({
</Button>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-2xl max-h-screen overflow-y-auto">
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>
{backupId ? "Update Backup" : "Create Backup"}
@@ -324,7 +324,7 @@ export const RestoreBackup = ({
Restore Backup
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center">
<RotateCcw className="mr-2 size-4" />
@@ -42,7 +42,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
See in detail the config of this container
</DialogDescription>
</DialogHeader>
<div className="text-wrap rounded-lg border p-4 text-sm bg-card overflow-y-auto max-h-[80vh]">
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
<code>
<pre className="whitespace-pre-wrap break-words">
<CodeEditor
@@ -1,13 +1,14 @@
import { Download as DownloadIcon, Loader2, Pause, Play } from "lucide-react";
import React, { useEffect, useRef } from "react";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { Download as DownloadIcon, Loader2 } from "lucide-react";
import React, { useEffect, useRef } from "react";
import { LineCountFilter } from "./line-count-filter";
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
import { StatusLogsFilter } from "./status-logs-filter";
import { TerminalLine } from "./terminal-line";
import { type LogLine, getLogType, parseLogs } from "./utils";
import { getLogType, type LogLine, parseLogs } from "./utils";
interface Props {
containerId: string;
@@ -61,6 +62,9 @@ export const DockerLogsId: React.FC<Props> = ({
const [showTimestamp, setShowTimestamp] = React.useState(true);
const [since, setSince] = React.useState<TimeFilter>("all");
const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
const [isPaused, setIsPaused] = React.useState(false);
const [messageBuffer, setMessageBuffer] = React.useState<string[]>([]);
const isPausedRef = useRef(false);
const scrollRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = React.useState(false);
@@ -85,15 +89,38 @@ export const DockerLogsId: React.FC<Props> = ({
const handleLines = (lines: number) => {
setRawLogs("");
setFilteredLogs([]);
setMessageBuffer([]);
setLines(lines);
};
const handleSince = (value: TimeFilter) => {
setRawLogs("");
setFilteredLogs([]);
setMessageBuffer([]);
setSince(value);
};
const handlePauseResume = () => {
if (isPaused) {
// Resume: Apply all buffered messages
if (messageBuffer.length > 0) {
const bufferedContent = messageBuffer.join("");
setRawLogs((prev) => {
const updated = prev + bufferedContent;
const splitLines = updated.split("\n");
if (splitLines.length > lines) {
return splitLines.slice(-lines).join("\n");
}
return updated;
});
setMessageBuffer([]);
}
}
const newPausedState = !isPaused;
setIsPaused(newPausedState);
isPausedRef.current = newPausedState;
};
useEffect(() => {
if (!containerId) return;
@@ -102,6 +129,10 @@ export const DockerLogsId: React.FC<Props> = ({
setIsLoading(true);
setRawLogs("");
setFilteredLogs([]);
setMessageBuffer([]);
// Reset pause state when container changes
setIsPaused(false);
isPausedRef.current = false;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const params = new globalThis.URLSearchParams({
@@ -140,14 +171,22 @@ export const DockerLogsId: React.FC<Props> = ({
ws.onmessage = (e) => {
if (!isCurrentConnection) return;
setRawLogs((prev) => {
const updated = prev + e.data;
const splitLines = updated.split("\n");
if (splitLines.length > lines) {
return splitLines.slice(-lines).join("\n");
}
return updated;
});
if (isPausedRef.current) {
// When paused, buffer the messages instead of displaying them
setMessageBuffer((prev) => [...prev, e.data]);
} else {
// When not paused, display messages normally
setRawLogs((prev) => {
const updated = prev + e.data;
const splitLines = updated.split("\n");
if (splitLines.length > lines) {
return splitLines.slice(-lines).join("\n");
}
return updated;
});
}
setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout);
};
@@ -210,9 +249,15 @@ export const DockerLogsId: React.FC<Props> = ({
});
};
// Sync isPausedRef with isPaused state
useEffect(() => {
isPausedRef.current = isPaused;
}, [isPaused]);
useEffect(() => {
setRawLogs("");
setFilteredLogs([]);
setMessageBuffer([]);
}, [containerId]);
useEffect(() => {
@@ -260,17 +305,48 @@ export const DockerLogsId: React.FC<Props> = ({
/>
</div>
<Button
variant="outline"
size="sm"
className="h-9 sm:w-auto w-full"
onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name}
>
<DownloadIcon className="mr-2 h-4 w-4" />
Download logs
</Button>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="h-9"
onClick={handlePauseResume}
title={isPaused ? "Resume logs" : "Pause logs"}
>
{isPaused ? (
<Play className="mr-2 h-4 w-4" />
) : (
<Pause className="mr-2 h-4 w-4" />
)}
{isPaused ? "Resume" : "Pause"}
</Button>
<Button
variant="outline"
size="sm"
className="h-9 sm:w-auto w-full"
onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name}
>
<DownloadIcon className="mr-2 h-4 w-4" />
Download logs
</Button>
</div>
</div>
{isPaused && (
<AlertBlock type="warning">
<div className="flex items-center gap-2">
<Pause className="h-4 w-4" />
<span>
Logs paused
{messageBuffer.length > 0 && (
<span className="ml-1 font-medium">
({messageBuffer.length} messages buffered)
</span>
)}
</span>
</div>
</AlertBlock>
)}
<div
ref={scrollRef}
onScroll={handleScroll}
@@ -40,7 +40,7 @@ export const ShowDockerModalLogs = ({
{children}
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogContent className="sm:max-w-7xl">
<DialogHeader>
<DialogTitle>View Logs</DialogTitle>
<DialogDescription>View the logs for {containerId}</DialogDescription>
@@ -40,7 +40,7 @@ export const ShowDockerModalStackLogs = ({
{children}
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
<DialogContent className="sm:max-w-7xl">
<DialogHeader>
<DialogTitle>View Logs</DialogTitle>
<DialogDescription>View the logs for {containerId}</DialogDescription>
@@ -60,7 +60,7 @@ export const DockerTerminalModal = ({
</DropdownMenuItem>
</DialogTrigger>
<DialogContent
className="max-h-screen overflow-y-auto sm:max-w-7xl"
className="sm:max-w-7xl"
onEscapeKeyDown={(event) => event.preventDefault()}
>
<DialogHeader>
@@ -97,7 +97,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify MariaDB</DialogTitle>
<DialogDescription>Update the MariaDB data</DialogDescription>
@@ -99,7 +99,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify MongoDB</DialogTitle>
<DialogDescription>Update the MongoDB data</DialogDescription>
@@ -123,7 +123,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
? queryError.message
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
</p>
<p className=" text-sm text-muted-foreground">URL: {baseUrl}</p>
<p className="text-sm text-muted-foreground">URL: {baseUrl}</p>
</div>
</div>
);
@@ -143,7 +143,7 @@ export const ShowPaidMonitoring = ({
? queryError.message
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
</p>
<p className=" text-sm text-muted-foreground">URL: {BASE_URL}</p>
<p className="text-sm text-muted-foreground">URL: {BASE_URL}</p>
</div>
</div>
);
@@ -97,7 +97,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify MySQL</DialogTitle>
<DialogDescription>Update the MySQL data</DialogDescription>
@@ -155,7 +155,7 @@ export function AddOrganization({ organizationId }: Props) {
control={form.control}
name="logo"
render={({ field }) => (
<FormItem className=" gap-4">
<FormItem className="gap-4">
<FormLabel className="text-right">Logo URL</FormLabel>
<FormControl>
<Input
@@ -169,7 +169,7 @@ export function AddOrganization({ organizationId }: Props) {
</FormItem>
)}
/>
<DialogFooter className="mt-4">
<DialogFooter>
<Button type="submit" isLoading={isLoading}>
{organizationId ? "Update organization" : "Create organization"}
</Button>
@@ -99,7 +99,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify Postgres</DialogTitle>
<DialogDescription>Update the Postgres data</DialogDescription>
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddTemplateSchema = z.object({
name: z.string().min(1, {
@@ -75,6 +75,8 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation();
@@ -119,7 +121,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
<span>Application</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Create</DialogTitle>
<DialogDescription>
@@ -155,68 +157,84 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{hasServers && (
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>App Name</FormLabel>
<FormLabel className="flex items-center gap-2">
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl>
<Input placeholder="my-app" {...field} />
</FormControl>
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddComposeSchema = z.object({
composeType: z.enum(["docker-compose", "stack"]).optional(),
@@ -78,6 +78,8 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddCompose>({
defaultValues: {
name: "",
@@ -124,7 +126,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
<span>Compose</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>Create Compose</DialogTitle>
<DialogDescription>
@@ -163,62 +165,64 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)}
/>
</div>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{hasServers && (
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application will be
deployed on the server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="appName"
@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import {
MariadbIcon,
MongodbIcon,
@@ -37,14 +43,14 @@ import {
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
type DbType = typeof mySchema._type.type;
@@ -163,6 +169,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddDatabase>({
defaultValues: {
type: "postgres",
@@ -283,7 +291,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
<span>Database</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen md:max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogContent className="md:max-h-[90vh] sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Databases</DialogTitle>
</DialogHeader>
@@ -374,45 +382,62 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a Server</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{hasServers && (
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a Server</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>App Name</FormLabel>
<FormLabel className="flex items-center gap-2">
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm
service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl>
<Input placeholder="my-app" {...field} />
</FormControl>
@@ -1,3 +1,18 @@
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import {
@@ -54,21 +69,6 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
@@ -137,6 +137,8 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
return matchesTags && matchesQuery;
}) || [];
const hasServers = servers && servers.length > 0;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full">
@@ -148,7 +150,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
<span>Template</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen sm:max-w-[90vw] p-0">
<DialogContent className="sm:max-w-[90vw] p-0">
<DialogHeader className="sticky top-0 z-10 bg-background p-6 border-b">
<div className="flex flex-col space-y-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
@@ -425,60 +427,62 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
project.
</AlertDialogDescription>
<div>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server{" "}
{!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the application
will be deployed on the server where the
user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{hasServers && (
<div>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server{" "}
{!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the
application will be deployed on the
server where the user is logged in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={(e) => {
setServerId(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
<Select
onValueChange={(e) => {
setServerId(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</span>
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
@@ -25,6 +25,7 @@ const examples = [
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example });
@@ -47,37 +48,39 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/>
</div>
<div className="space-y-2">
<Label htmlFor="server-deploy">
Select the server where you want to deploy (optional)
</Label>
<Select
value={templateInfo.server?.serverId}
onValueChange={(value) => {
const server = servers?.find((s) => s.serverId === value);
if (server) {
setTemplateInfo({
...templateInfo,
server: server,
});
}
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem key={server.serverId} value={server.serverId}>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
{hasServers && (
<div className="space-y-2">
<Label htmlFor="server-deploy">
Select the server where you want to deploy (optional)
</Label>
<Select
value={templateInfo.server?.serverId}
onValueChange={(value) => {
const server = servers?.find((s) => s.serverId === value);
if (server) {
setTemplateInfo({
...templateInfo,
server: server,
});
}
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem key={server.serverId} value={server.serverId}>
{server.name}
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<Label>Examples:</Label>
@@ -199,7 +199,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<p className="text-muted-foreground">
Generating template suggestions based on your input...
</p>
<pre>{templateInfo.userInput}</pre>
<pre className="whitespace-normal">{templateInfo.userInput}</pre>
</div>
);
}
@@ -259,7 +259,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<AccordionItem value="description">
<AccordionTrigger>Description</AccordionTrigger>
<AccordionContent>
<ScrollArea className=" w-full rounded-md border p-4">
<ScrollArea className="w-full rounded-md border p-4">
<ReactMarkdown className="text-muted-foreground text-sm">
{selectedVariant?.description}
</ReactMarkdown>
@@ -289,7 +289,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<AccordionItem value="env-variables">
<AccordionTrigger>Environment Variables</AccordionTrigger>
<AccordionContent>
<ScrollArea className=" w-full rounded-md border">
<ScrollArea className="w-full rounded-md border">
<div className="p-4 space-y-4">
{selectedVariant?.envVariables.map((env, index) => (
<div
@@ -364,7 +364,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<AccordionItem value="domains">
<AccordionTrigger>Domains</AccordionTrigger>
<AccordionContent>
<ScrollArea className=" w-full rounded-md border">
<ScrollArea className="w-full rounded-md border">
<div className="p-4 space-y-4">
{selectedVariant?.domains.map((domain, index) => (
<div
@@ -158,7 +158,7 @@ export const TemplateGenerator = ({ projectId }: Props) => {
<span>AI Assistant</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl w-full flex flex-col">
<DialogContent className="sm:max-w-4xl w-full flex flex-col">
<DialogHeader>
<DialogTitle>AI Assistant</DialogTitle>
<DialogDescription>
@@ -94,7 +94,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
</DropdownMenuItem>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-6xl">
<DialogContent className="sm:max-w-6xl">
<DialogHeader>
<DialogTitle>Project Environment</DialogTitle>
<DialogDescription>
@@ -1,3 +1,17 @@
import {
AlertTriangle,
ArrowUpDown,
BookIcon,
ExternalLinkIcon,
FolderInput,
Loader2,
MoreHorizontalIcon,
Search,
TrashIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
@@ -31,20 +45,14 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import {
AlertTriangle,
BookIcon,
ExternalLinkIcon,
FolderInput,
Loader2,
MoreHorizontalIcon,
Search,
TrashIcon,
} from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import { toast } from "sonner";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { HandleProject } from "./handle-project";
import { ProjectEnvironment } from "./project-environment";
@@ -54,15 +62,65 @@ export const ShowProjects = () => {
const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
const [searchQuery, setSearchQuery] = useState("");
const [sortBy, setSortBy] = useState<string>(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("projectsSort") || "createdAt-desc";
}
return "createdAt-desc";
});
useEffect(() => {
localStorage.setItem("projectsSort", sortBy);
}, [sortBy]);
const filteredProjects = useMemo(() => {
if (!data) return [];
return data.filter(
// First filter by search query
const filtered = data.filter(
(project) =>
project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
project.description?.toLowerCase().includes(searchQuery.toLowerCase()),
);
}, [data, searchQuery]);
// Then sort the filtered results
const [field, direction] = sortBy.split("-");
return [...filtered].sort((a, b) => {
let comparison = 0;
switch (field) {
case "name":
comparison = a.name.localeCompare(b.name);
break;
case "createdAt":
comparison =
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break;
case "services": {
const aTotalServices =
a.mariadb.length +
a.mongo.length +
a.mysql.length +
a.postgres.length +
a.redis.length +
a.applications.length +
a.compose.length;
const bTotalServices =
b.mariadb.length +
b.mongo.length +
b.mysql.length +
b.postgres.length +
b.redis.length +
b.applications.length +
b.compose.length;
comparison = aTotalServices - bTotalServices;
break;
}
default:
comparison = 0;
}
return direction === "asc" ? comparison : -comparison;
});
}, [data, searchQuery, sortBy]);
return (
<>
@@ -98,14 +156,40 @@ export const ShowProjects = () => {
</div>
) : (
<>
<div className="w-full relative">
<Input
placeholder="Filter projects..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pr-10"
/>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<div className="flex max-sm:flex-col gap-4 items-center w-full">
<div className="flex-1 relative max-sm:w-full">
<Input
placeholder="Filter projects..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pr-10"
/>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div>
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
<ArrowUpDown className="size-4 text-muted-foreground" />
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Sort by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
<SelectItem value="createdAt-desc">
Newest first
</SelectItem>
<SelectItem value="createdAt-asc">
Oldest first
</SelectItem>
<SelectItem value="services-desc">
Most services
</SelectItem>
<SelectItem value="services-asc">
Least services
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{filteredProjects?.length === 0 && (
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
@@ -97,7 +97,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify Redis</DialogTitle>
<DialogDescription>Update the redis data</DialogDescription>
@@ -47,7 +47,7 @@ export const columns: ColumnDef<LogEntry>[] = [
cell: ({ row }) => {
const log = row.original;
return (
<div className=" flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="flex items-center flex-row gap-3 ">
{log.RequestMethod}{" "}
<div className="inline-flex items-center gap-2 bg-muted px-1.5 py-1 rounded-lg">
@@ -86,7 +86,7 @@ export const columns: ColumnDef<LogEntry>[] = [
cell: ({ row }) => {
const log = row.original;
return (
<div className=" flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="flex flex-row gap-3 w-full">
{format(new Date(log.StartUTC), "yyyy-MM-dd HH:mm:ss")}
</div>
@@ -142,7 +142,7 @@ export const AddApiKey = () => {
<DialogTrigger asChild>
<Button>Generate New Key</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
<DialogContent className="sm:max-w-xl max-h-[90vh]">
<DialogHeader>
<DialogTitle>Generate API Key</DialogTitle>
<DialogDescription>
@@ -171,7 +171,7 @@ export const ShowBilling = () => {
)}
{isAnnual ? (
<div className="flex flex-row gap-2 items-center">
<p className=" text-2xl font-semibold tracking-tight text-primary ">
<p className="text-2xl font-semibold tracking-tight text-primary ">
${" "}
{calculatePrice(
serverQuantity,
@@ -180,7 +180,7 @@ export const ShowBilling = () => {
USD
</p>
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
<p className="text-base font-semibold tracking-tight text-muted-foreground">
${" "}
{(
calculatePrice(serverQuantity, isAnnual) / 12
@@ -189,7 +189,7 @@ export const ShowBilling = () => {
</p>
</div>
) : (
<p className=" text-2xl font-semibold tracking-tight text-primary ">
<p className="text-2xl font-semibold tracking-tight text-primary ">
${" "}
{calculatePrice(serverQuantity, isAnnual).toFixed(
2,
@@ -41,7 +41,7 @@ export const ShowWelcomeDokploy = () => {
return (
<>
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-xl max-h-screen overflow-y-auto">
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle className="text-2xl font-semibold text-center">
Welcome to Dokploy Cloud 🎉

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