Compare commits

...

143 Commits

Author SHA1 Message Date
Mauricio Siu
0bdaa81263 fix(backups): replace findAdmin with db query to fetch admin by role for cron job initialization 2025-04-02 07:10:19 -06:00
Mauricio Siu
baf36b6fb6 Merge pull request #1608 from Dokploy/fix/move-cron-to-after-migration
Fix/move cron to after migration
2025-04-02 06:58:38 -06:00
Mauricio Siu
d632e83799 Update dokploy version to v0.21.2 in package.json 2025-04-02 06:53:41 -06:00
Mauricio Siu
6f52edd845 fix(backups): ensure initCronJobs is awaited before migration to improve backup reliability 2025-04-02 06:53:32 -06:00
Mauricio Siu
9d0f5bc8cd Update package.json 2025-04-02 00:31:55 -06:00
Mauricio Siu
3dc558c260 Merge pull request #1599 from vicke4/backup-cron-fix-attempt
fix(backups): awaiting initCronJobs call in an attempt to fix backups cron
2025-04-02 00:31:20 -06:00
vicke4
180aa34140 fix(backups): awaiting initcronjobs in an attempt to fix backups cron 2025-04-02 11:07:48 +05:30
Mauricio Siu
ffc85b04a8 Merge pull request #1572 from thebadking/refactor-show-build-form-and-prettier
build form optimization
2025-03-30 03:36:19 -06:00
Mauricio Siu
dbcfc702d4 Merge branch 'canary' into refactor-show-build-form-and-prettier 2025-03-30 03:31:49 -06:00
Mauricio Siu
7805efc738 Merge pull request #1584 from Dokploy/1294-duplicate-project-andor-services-accross-projects
1294 duplicate project andor services accross projects
2025-03-30 02:50:09 -06:00
Mauricio Siu
3910e22412 Refactor Duplicate Project component and integrate with project detail page
- Updated the DuplicateProject component to accept projectId and selectedServiceIds as props, enhancing its flexibility.
- Removed unnecessary state management for service selection within the component.
- Integrated the DuplicateProject component directly into the project detail page, allowing for easier access to duplication functionality.
- Improved the user interface by utilizing DialogTrigger for initiating the duplication process and displaying selected services more clearly.
2025-03-30 02:42:35 -06:00
Mauricio Siu
2f16034cb0 Add Duplicate Project functionality
- Introduced a new component for duplicating projects, allowing users to create a new project with the same configuration as an existing one.
- Implemented a mutation in the project router to handle project duplication, including optional service duplication.
- Updated the project detail page to include a dropdown menu for initiating the duplication process.
- Enhanced the API to validate and process the duplication request, ensuring proper handling of services associated with the project.
2025-03-30 02:38:53 -06:00
Mauricio Siu
d4925dd2b7 Update dokploy version to v0.21.0 in package.json 2025-03-30 01:59:43 -06:00
Mauricio Siu
5aba6c79a0 Merge pull request #1582 from Dokploy/fix/allow-false-values-in-env
Fix/allow false values in env
2025-03-30 01:40:06 -06:00
Mauricio Siu
84f5627471 Enhance environment variable handling in processTemplate: support boolean and number types in env configuration, with tests for both array and object formats. 2025-03-30 01:29:23 -06:00
Mauricio Siu
4eaf8fee0f Add toml package and update configuration parsing in Dokploy
- Added toml package version 3.0.0 to package.json files in both apps/dokploy and packages/server.
- Replaced js-yaml load function with toml parse function for configuration parsing in compose.ts and github.ts files, ensuring compatibility with TOML format.
- Updated pnpm-lock.yaml to reflect the new toml dependency.
2025-03-30 01:12:27 -06:00
Mauricio Siu
adee87b6da Enhance volume handling in Docker Compose: update addSuffixToVolumesInServices to correctly manage volume paths with subdirectories and improve test coverage for suffix changes in volume names. 2025-03-30 00:32:09 -06:00
Mauricio Siu
e5e987fcf9 Merge branch 'canary' into fix/allow-false-values-in-env 2025-03-29 23:51:48 -06:00
Mauricio Siu
d0a6373dcc Update Dockerfile to include 'zip' package in production environment for enhanced file handling capabilities. 2025-03-29 23:49:45 -06:00
Mauricio Siu
8ed44066ad Update Card component in server settings page: adjust width class for better responsiveness and layout consistency. 2025-03-29 23:38:28 -06:00
Mauricio Siu
befe2193a7 Merge pull request #1581 from Dokploy/709-back-ups-for-dokploy
709 back ups for dokploy
2025-03-29 23:27:29 -06:00
Mauricio Siu
f20c73cdee Refactor temporary directory creation in web server backup: replace static path with dynamic temp directory using mkdtemp for improved file management and isolation during backup operations. 2025-03-29 23:27:13 -06:00
Mauricio Siu
16bfc09202 Refactor serverId assignment in ShowBackups component: update logic to check for serverId presence in postgres object, improving code clarity and consistency. 2025-03-29 20:24:39 -06:00
Mauricio Siu
d54a61b2a4 Remove commented-out backup commands from restoreWebServerBackup function for cleaner code and improved readability. 2025-03-29 20:14:55 -06:00
Mauricio Siu
60c09a6434 Implement cloud check in backup and restore functions: add IS_CLOUD condition to skip operations for cloud environments in runWebServerBackup and restoreWebServerBackup functions. 2025-03-29 20:14:36 -06:00
Mauricio Siu
5361e9074f Refactor backup scheduling: consolidate backup handling for multiple database types into a single loop, enhance error logging, and integrate web server backup functionality. 2025-03-29 20:12:55 -06:00
Mauricio Siu
13d4dea504 Enhance ShowBackups component: add Database icon to title for improved UI clarity and wrap ShowBackups in a Card component for better layout in server settings page. 2025-03-29 19:53:25 -06:00
Mauricio Siu
ffc2d593e4 Refactor restoreWebServerBackup function: implement temporary directory creation outside BASE_PATH, streamline filesystem restoration process, and enhance logging for database restore operations. 2025-03-29 19:47:02 -06:00
Mauricio Siu
297439a348 Update restore-backup component and backup router for web server support: set default database name based on type, disable input for web server, and streamline backup restoration process with improved logging and error handling. 2025-03-29 19:27:05 -06:00
Mauricio Siu
ff3e067866 Implement web server backup and restore functionality: add new backup and restore methods for web servers, including S3 integration and improved logging. Refactor existing backup process to support web server type and streamline temporary file management. 2025-03-29 18:43:35 -06:00
Mauricio Siu
f008a45bf2 Enhance backup process: implement temporary directory structure for backups, add filesystem backup, and create zip file of database and filesystem contents. Update logging for better visibility during backup operations. 2025-03-29 18:00:13 -06:00
Mauricio Siu
50c8503cf9 Enhance backup functionality: add support for 'web-server' database type in backup components, update related schemas, and implement new backup procedures. Introduce userId reference in backups schema and adjust UI components to accommodate the new type. 2025-03-29 17:53:57 -06:00
Mauricio Siu
930a03de60 Merge pull request #1523 from jrparks/feat/add-gitea-repo
Feat/add gitea repo
2025-03-29 15:43:55 -06:00
autofix-ci[bot]
2d3d86e823 [autofix.ci] apply automated fixes 2025-03-29 21:18:08 +00:00
André Ferreira
7bab166e1b increased type safety 2025-03-29 21:17:32 +00:00
Mauricio Siu
7a6e1dbc1b Refactor SecurityAudit component: remove unused root login security check and related UI elements to streamline the code and improve readability. 2025-03-29 15:04:33 -06:00
Mauricio Siu
17a859d26d Refactor ShowConvertedCompose component: simplify conditional rendering of the compose file display by removing unnecessary null check and ensuring consistent layout. 2025-03-29 15:01:55 -06:00
Mauricio Siu
d793c6a2ec Add Gitea repository link in SaveGiteaProvider: integrate dynamic repository URL display and enhance user experience with a view link for existing repositories. 2025-03-29 15:00:23 -06:00
Mauricio Siu
3adb9d54f4 Refactor ShowProviderFormCompose component: streamline provider rendering logic and ensure Gitea integration is fully functional with updated tab and content handling. 2025-03-29 14:57:36 -06:00
Mauricio Siu
7144adbf0c Add migration for '0081_lovely_mentallo': remove gitea_username column and update journal with new version 2025-03-29 14:47:51 -06:00
Mauricio Siu
55328468d1 Refactor Gitea integration: remove giteaProjectId references and update related schemas. Add new fields for gitea repository details in application tests and components. 2025-03-29 14:44:33 -06:00
Mauricio Siu
6968cb6930 Merge pull request #1465 from zaaakher/docs/guides
docs: update `CONTRIBUTING.md` and add `GUIDES.md`
2025-03-29 14:28:24 -06:00
Mauricio Siu
a431e4c58e Update CONTRIBUTING.md 2025-03-29 14:28:11 -06:00
Mauricio Siu
fe967239b4 Merge branch 'canary' into feat/add-gitea-repo 2025-03-29 13:47:18 -06:00
Mauricio Siu
c5b4b85470 Merge pull request #1578 from Dokploy/fix/biome-lint
chore(workflow): add Biome code formatting workflow for canary branch
2025-03-29 13:41:49 -06:00
Mauricio Siu
b1ef9d25b1 chore(workflow): add autofix.ci workflow for automatic code formatting on canary branch 2025-03-29 13:41:05 -06:00
Mauricio Siu
74f7c51530 chore(workflow): add autofix.ci workflow for automatic code formatting with Biome 2025-03-29 13:39:57 -06:00
Mauricio Siu
4ba2b9fe8d chore(workflow): add new Biome formatting workflow for canary branch 2025-03-29 13:38:55 -06:00
Mauricio Siu
413eda50f4 chore(workflow): simplify AutoFix action usage in Biome workflow 2025-03-29 13:37:43 -06:00
Mauricio Siu
9f09681708 chore(workflow): streamline Biome setup by replacing Node.js and pnpm steps with biomeJs action 2025-03-29 13:37:01 -06:00
Mauricio Siu
8eb174812d chore(workflow): replace manual commit step with AutoFix action for Biome formatting 2025-03-29 13:35:50 -06:00
Mauricio Siu
be77f114eb Merge ca42708035 into beadcf871a 2025-03-29 19:30:24 +00:00
Mauricio Siu
ca42708035 chore(workflow): configure git user for automated commits and enforce push 2025-03-29 13:30:18 -06:00
Mauricio Siu
8b03454a87 chore(workflow): update Biome workflow to push changes to the correct branch 2025-03-29 13:28:50 -06:00
Mauricio Siu
fa7f749f84 refactor(dokploy): standardize code formatting and improve readability across multiple components 2025-03-29 13:26:44 -06:00
Mauricio Siu
3daecd7d71 chore(workflow): update pnpm version to 9.5.0 in Biome workflow 2025-03-29 13:20:48 -06:00
Mauricio Siu
0666b5b292 chore(workflow): add pnpm setup step to Biome workflow 2025-03-29 13:20:07 -06:00
Mauricio Siu
b288ddd826 chore(workflow): add Biome code formatting workflow for canary branch 2025-03-29 13:18:50 -06:00
Mauricio Siu
beadcf871a Merge pull request #1577 from Dokploy/1568-dokploy---nextjs-affected-by-cve-2025-29927
refactor(dokploy): remove lucia-auth adapter and related authenticati…
2025-03-29 12:23:26 -06:00
Mauricio Siu
ee49dadf0b refactor(dokploy): remove lucia-auth adapter and related authentication logic; update next.js version to 15.2.4 2025-03-29 12:17:14 -06:00
Mauricio Siu
46de83a1de Merge pull request #1576 from Dokploy/1564-downloaded-ssh-keys-are-always-named-as-id_rsa-keys
refactor(ssh-keys): simplify downloadKey function and filename genera…
2025-03-29 12:13:38 -06:00
Mauricio Siu
fee5024b7d refactor(ssh-keys): simplify downloadKey function and filename generation logic 2025-03-29 12:13:22 -06:00
André Ferreira
1f28a21835 remove prettier 2025-03-28 19:21:39 +00:00
André Ferreira
0114b371f5 prettier and build form optimization 2025-03-28 17:31:53 +00:00
Jason Parks
66d6cb5710 Fixed compose bug and formatted. Updated the refresh token to check the expired time. 2025-03-27 15:27:53 -06:00
Jason Parks
5927c7c3c5 Added back in compose alert block 2025-03-27 13:26:24 -06:00
Jason Parks
84afcf0de5 Removed apps/dokploy/templates/infisical/index.ts 2025-03-27 13:12:02 -06:00
Mauricio Siu
e3527f7d69 fix(processors): ensure environment variable processing handles non-string values correctly 2025-03-26 01:48:50 -06:00
Jason Parks
39f4a35cc8 fix: remove giteaPathNamespace 2025-03-24 01:53:00 -06:00
Mauricio Siu
e0433e9f7b Merge pull request #1554 from Dokploy/1061-custom-docker-service-hostname
1061 custom docker service hostname
2025-03-23 23:58:28 -06:00
Mauricio Siu
d29ff881fc fix(redis-connection): update Redis host configuration to use environment variable for production 2025-03-23 23:43:38 -06:00
Mauricio Siu
568c3a1d06 chore(workflow): update branches for Dokploy Docker build to use custom hostname 2025-03-23 23:38:55 -06:00
Mauricio Siu
e9fd280fa2 refactor(server): comment out initialization of Postgres, Traefik, and Redis for testing purposes 2025-03-23 23:28:53 -06:00
Jason Parks
5d5913f39d fix: resolve aria-hidden accessibility error in dialog component
Remove custom onOpenAutoFocus handler that was preventing proper focus management in the EditGiteaProvider dialog. This fixes the 'Blocked aria-hidden on an element because its descendant retained focus' error by allowing the dialog component to handle focus naturally.
2025-03-23 16:13:16 -06:00
Jason Parks
f04c8a36af Fix Gitea repository integration and preview compose display 2025-03-23 15:35:22 -06:00
Jason Parks
d5137d5d3a fix: handle repository id null case consistently 2025-03-23 14:04:30 -06:00
Mauricio Siu
9535fca28f Merge pull request #1540 from vicke4/backup-deletion-fix
fix(backups): auto deletion of backups
2025-03-23 04:31:37 -06:00
Mauricio Siu
dd62d603e0 Merge pull request #1550 from Dokploy/1543-preview-docker-compose-button-null-when-git-is-provider
feat(dashboard): add informational alert for docker-compose preview r…
2025-03-23 04:30:58 -06:00
Mauricio Siu
8d227e2a2c feat(dashboard): add informational alert for docker-compose preview requirements 2025-03-23 04:30:00 -06:00
Mauricio Siu
048c8ffc11 feat: Add Docker image push notification in commit message extraction
- Enhanced the extractCommitMessage function to include a specific message format for Docker image pushes when the user-agent indicates a Go-http-client. This improves clarity in deployment notifications by providing detailed information about the repository and the pusher.
2025-03-23 04:12:08 -06:00
Mauricio Siu
b59597630c refactor: Remove commented-out Gitea requirements check in Git providers component
- Eliminated unnecessary commented code related to Gitea access token checks in the ShowGitProviders component to enhance code clarity and maintainability.
2025-03-23 04:10:00 -06:00
Mauricio Siu
707463f973 refactor: Streamline Docker service management and error handling
- Removed unnecessary console logging in the mechanizeDockerContainer function to enhance code clarity.
- Simplified error handling by directly creating a new service if the existing one is not found, improving the deployment logic.
- Updated the buildNixpacks function to ensure container cleanup is attempted without additional error handling, streamlining the process.
2025-03-23 04:09:06 -06:00
Mauricio Siu
4b3e0805a4 chore: Update dependencies and clean up code formatting
- Downgraded '@trpc/server' version in package.json and pnpm-lock.yaml to match other TRPC packages.
- Removed unused dependencies 'node-fetch', 'data-uri-to-buffer', 'fetch-blob', and 'formdata-polyfill' from pnpm-lock.yaml and package.json.
- Improved code formatting in various components for better readability, including adjustments in 'edit-gitea-provider.tsx' and 'security-audit.tsx'.
- Refactored imports in 'auth.ts' for better organization and clarity.
2025-03-23 04:06:35 -06:00
Mauricio Siu
148c30f604 refactor: Remove unnecessary logging in Gitea repository fetching
- Eliminated console logging of repositories in the Gitea API router to streamline the code and improve performance.
2025-03-23 03:57:37 -06:00
Mauricio Siu
95f79f2afb feat: Enhance deployment logic for multiple Git providers
- Added support for handling commit normalization across GitHub, GitLab, and Gitea in the deployment API.
- Implemented a new utility function to determine the provider based on request headers.
- Improved deployment path validation to ensure consistency across different source types.
- Cleaned up the code by removing redundant checks and enhancing readability.
2025-03-23 03:55:11 -06:00
Mauricio Siu
a067abd3e4 refactor: Enhance Gitea repository handling and clean up token refresh logic
- Updated Gitea repository cloning to use the remote variant for better consistency.
- Removed unnecessary logging and comments in the token refresh function to streamline the code.
- Improved type handling in the cloneGiteaRepository function for better clarity.
2025-03-23 03:35:02 -06:00
Mauricio Siu
9359ee7a04 refactor: Update Gitea provider components and API handling
- Adjusted GiteaProviderSchema to ensure watchPaths are correctly initialized and validated.
- Refactored SaveGiteaProvider and SaveGiteaProviderCompose components for improved state management and UI consistency.
- Simplified API router methods for Gitea, enhancing readability and error handling.
- Updated database schema and service functions for better clarity and maintainability.
- Removed unnecessary comments and improved logging for better debugging.
2025-03-23 03:27:19 -06:00
Jason Parks
fc7eff94b6 fix: Security Audit SSH Errors #1377
- Fixed SSH key authentication detection in server-audit.ts
- Added proper handling for prohibit-password and other secure root login options
- Fixed typos in security audit UI labels
- Improved error handling with optional chaining
2025-03-22 14:26:40 -06:00
Jason Parks
ff3d444b89 fix: prevent form dropdown flicker in Gitea provider modal
Prevents the brief appearance of dropdown options when opening the Edit Gitea Provider modal by:
- Adding onOpenAutoFocus event handler to prevent automatic focus
- Setting autoFocus={false} on the first input field
- Simplifying component state management

This improves the UI experience by eliminating visual artifacts when the dialog opens.
2025-03-22 13:30:47 -06:00
Jason Parks
530ad31aaa Simplify Gitea authorization flow with shared utilities 2025-03-20 16:48:59 -06:00
vicke4
68d0a48843 fix(backups): auto deletion of backups 2025-03-21 01:36:11 +05:30
Jason Parks
a4e4d1c467 Fix Gitea watch branch validation logic 2025-03-20 13:50:55 -06:00
Jason Parks
56d8defebe Added watchlist paths for Gitea and some minor typescript fixes. 2025-03-19 16:48:51 -06:00
Jason Parks
997e755b6f Merge remote-tracking branch 'upstream/canary' into feat/add-gitea-repo 2025-03-19 14:06:29 -06:00
Jason Parks
852011dde8 Merge branch 'feat/add-gitea-repo' of https://github.com/jrparks/dokploy into feat/add-gitea-repo 2025-03-19 11:32:29 -06:00
Jason Parks
d7ef201adb Refactor Gitea integration and update related components 2025-03-19 11:31:08 -06:00
Mauricio Siu
91183056f0 Merge pull request #1534 from Dokploy/feat/enable-swarm-overview
Feat/enable swarm overview
2025-03-19 00:52:22 -06:00
Mauricio Siu
03bd4398d0 chore(package): bump version to v0.20.8 2025-03-19 00:51:49 -06:00
Mauricio Siu
8c260eff72 feat(cluster): enhance AddNode and ShowNodes components for better user guidance and functionality
- Added an AlertBlock in AddNode to inform users about architecture compatibility when adding nodes.
- Updated ShowNodes to correctly handle node deletion actions based on ManagerStatus.
- Refactored cluster API to remove cloud-specific checks and improve command execution for remote servers.
2025-03-19 00:51:27 -06:00
Mauricio Siu
4eef65f1b7 fix(git): update Gitea provider instructions and improve Git providers display
- Enhanced the Gitea provider setup instructions by specifying the navigation path for creating a new OAuth2 application.
- Added a blank line for better readability in the Git providers display component.
2025-03-18 21:50:01 -06:00
Mauricio Siu
a7535c6862 Merge branch 'canary' into feat/add-gitea-repo 2025-03-18 21:42:42 -06:00
Mauricio Siu
6e28196b0e chore(package): bump version to v0.20.7 2025-03-18 21:36:39 -06:00
Mauricio Siu
18bacae175 Merge pull request #1507 from nb5p/fix-alpine-linux-compatibility
fix(server-setup): resolve Alpine Linux compatibility issues
2025-03-18 21:35:43 -06:00
Mauricio Siu
f2be5a378e Merge pull request #1522 from ensarkurrt/canary
fix(ui): Improve Numeric Input Handling in Swarm Cluster Settings, Traefik Port Mappings, and Email Notifications
2025-03-18 21:27:20 -06:00
Mauricio Siu
aef24296b9 Merge pull request #1531 from Dokploy/fix/loader-swarm
Fix/loader swarm
2025-03-18 21:18:17 -06:00
Mauricio Siu
7123b9b109 feat(cluster): add error handling in AddManager and AddWorker components
- Integrated error handling in AddManager and AddWorker components to display error messages using AlertBlock when data fetching fails.
- Updated API query hooks to include error and isError states for improved user feedback during data operations.
2025-03-18 21:17:11 -06:00
Mauricio Siu
891dc840f5 feat(cluster): enhance node management UI with loading indicators and improved tab content
- Added loading indicators in AddManager and AddWorker components to enhance user experience during data fetching.
- Updated AddNode component to include overflow handling for tab content.
- Renamed "Show Nodes" to "Show Swarm Nodes" in ShowNodesModal for clarity.
2025-03-18 21:11:50 -06:00
Zakher Masri
bc78100613 remove redis part 2025-03-18 12:02:03 +03:00
Mauricio Siu
ff22404b3b feat(gitea): add Gitea integration with database schema updates
- Introduced Gitea support by adding necessary database tables and columns.
- Updated enum types to include 'gitea' for source and git provider types.
- Established foreign key relationships between Gitea and application/compose tables.
- Removed obsolete Gitea-related SQL files and updated journal entries for clarity.
2025-03-18 01:16:38 -06:00
Mauricio Siu
17330ca71a refactor: standardize import statements and improve code structure across multiple components
- Updated import statements to maintain consistency and clarity.
- Refactored components to enhance readability and organization.
- Ensured proper usage of type imports and removed unnecessary comments.
- Improved user feedback mechanisms in forms and alerts for better user experience.
2025-03-18 00:52:34 -06:00
Mauricio Siu
2898a5e575 Merge branch 'canary' into feat/add-gitea-repo 2025-03-18 00:50:27 -06:00
Mauricio Siu
172694be30 Merge pull request #1530 from Dokploy/feat/add-date-to-restore-item
feat(backup): enhance RestoreBackup component and API to include serv…
2025-03-18 00:49:02 -06:00
Mauricio Siu
ea6cfc9d29 feat(backup): enhance RestoreBackup component and API to include serverId
- Added serverId prop to RestoreBackup component for better context during backup restoration.
- Updated ShowBackups component to pass serverId from the Postgres object.
- Modified backup API to handle serverId, allowing remote execution of backup commands when specified.
- Improved file display in RestoreBackup for better user experience.
2025-03-18 00:47:50 -06:00
Mauricio Siu
4fa5e10789 chore(package): bump version to v0.20.6 2025-03-18 00:18:39 -06:00
Mauricio Siu
cb7fbb777c Merge pull request #1528 from Dokploy/1524-getting-502-bad-gateway
1524 getting 502 bad gateway
2025-03-18 00:15:12 -06:00
Mauricio Siu
6a388fe370 feat(domain): add validation for traefik.me domain IP address requirement
- Implemented a check to ensure an IP address is set for traefik.me domains in the AddDomain and AddDomainCompose components.
- Integrated a new API query to determine if traefik.me domains can be generated based on the server's IP address.
- Added user feedback through alert messages when the IP address is not configured.
2025-03-18 00:13:55 -06:00
Mauricio Siu
0722182650 feat(auth): implement user creation validation and IP update logic
- Added validation for user creation to check for existing admin presence and validate x-dokploy-token.
- Integrated public IP retrieval for user updates when not in cloud environment.
- Enhanced error handling with APIError for better feedback during user creation process.
2025-03-17 23:59:39 -06:00
Mauricio Siu
5e1095d199 Merge pull request #1526 from Dokploy/fix/mongo-db-button-deploy
refactor: improve code formatting and structure in ShowGeneralMongo c…
2025-03-17 23:18:18 -06:00
Mauricio Siu
c80a31e8c4 refactor: improve code formatting and structure in ShowGeneralMongo component
- Standardized indentation and formatting for better readability.
- Enhanced tooltip integration within button elements for improved user experience.
- Maintained functionality for deploying, reloading, starting, and stopping MongoDB instances while ensuring consistent code style.
2025-03-17 23:16:29 -06:00
Jason Parks
fac8ea7a30 Merge branch 'canary' into feat/add-gitea-repo 2025-03-17 15:25:02 -06:00
Jason Parks
9a11d0db97 feat(gitea): add Gitea repository support 2025-03-17 15:17:35 -06:00
Ensar Kurt
3cdf4c426c revert commit from #1513 2025-03-18 00:05:59 +03:00
Ensar Kurt
7cb184dc97 email notification port, last digit staying error fix 2025-03-17 23:48:17 +03:00
Ensar Kurt
fe57333f84 manage port inputs, default zero fix 2025-03-17 23:47:54 +03:00
Ensar Kurt
04fd77c3a9 replicas input cannot be zero and empty 2025-03-17 23:42:09 +03:00
Mauricio Siu
7c17cfb5c7 refactor: improve button structure and tooltip integration across dashboard components
- Refactored button components in the dashboard to enhance structure and readability.
- Integrated tooltips directly within button elements for better user experience.
- Updated tooltip descriptions for clarity across various database actions (Deploy, Reload, Start, Stop) for Redis, MySQL, PostgreSQL, and MariaDB.
- Ensured consistent formatting and improved code maintainability.
2025-03-16 20:52:57 -06:00
Mauricio Siu
c6a288781f Merge pull request #1516 from Dokploy/1475-multiple-deployments-triggered-for-a-single-action-when-using-multiple-organizations-linked-to-the-same-github-account
fix(api): enhance GitHub deployment handling with additional GitHub …
2025-03-16 20:19:16 -06:00
Mauricio Siu
724bed9832 feat(api): enhance GitHub deployment handling with additional GitHub ID checks
- Added GitHub ID checks to the deployment logic for applications and composes.
- Improved the extraction of deployment title and hash from the request headers and body.
- Ensured consistency in handling deployment data across different branches and repositories.
2025-03-16 20:15:51 -06:00
Mauricio Siu
2405e5a93a refactor: standardize code formatting and improve component structure across dashboard components
- Updated component props formatting for consistency.
- Refactored API query hooks and mutation calls for better readability.
- Enhanced tooltip descriptions for clarity in user actions.
- Maintained functionality for deploying, reloading, starting, and stopping applications, composes, and Postgres instances.
2025-03-16 19:50:04 -06:00
Mauricio Siu
e97c8f42b3 chore(package): bump version to v0.20.5 2025-03-16 19:45:48 -06:00
Mauricio Siu
d805f6a7aa Merge pull request #1510 from Alm0stEthical/canary
Fix: Consistent Component Styling and Server URL
2025-03-16 19:09:26 -06:00
Mauricio Siu
45d05b2aa4 Merge pull request #1514 from Dokploy/338-how-to-restore-a-database-backup
338 how to restore a database backup
2025-03-16 19:00:10 -06:00
Mauricio Siu
6d350a23a9 feat(tests): add cleanCache property to baseApp in drop and traefik test files 2025-03-16 18:57:41 -06:00
Mauricio Siu
5965b73342 Merge pull request #1513 from ensarkurrt/canary
fix(ui): Prevent Zero from Persisting in Numeric Input
2025-03-16 18:56:59 -06:00
Mauricio Siu
b8e06feaff refactor(show-backups): remove commented-out restore backup section 2025-03-16 18:53:55 -06:00
Mauricio Siu
3c5a005165 feat(backup): implement restore backup functionality
- Added a new component `RestoreBackup` for restoring database backups.
- Integrated the restore functionality with a form to select destination, backup file, and database name.
- Implemented API endpoints for listing backup files and restoring backups with logs.
- Enhanced the `ShowBackups` component to include the `RestoreBackup` option alongside existing backup features.
2025-03-16 18:53:20 -06:00
Ensar KURT
12d31c89f3 If number input is empty, make 0 when focus is lost 2025-03-17 01:25:14 +03:00
Jason Parks
cf28640188 Merge branch 'Dokploy:canary' into canary 2025-03-16 13:13:41 -06:00
David Tanasescu
3cf7c697b8 Fix: Consistent Component Styling and Server URL 2025-03-16 13:36:42 +01:00
Jason Parks
ea39b152f4 fix: resolved merge conflicts with fork/canary 2025-03-16 03:02:15 -06:00
Jason Parks
027406547e feat(gitea): Added Gitea Repo Integration 2025-03-16 02:11:48 -06:00
nb5p
2974a8183e fix(server-setup): resolve Alpine Linux compatibility issues with setup scripts
Resolves #1482
2025-03-16 15:37:28 +08:00
Zakher Masri
ac0922d742 docs: update CONTRIBUTING.md and add GUIDES.md 2025-03-11 14:38:37 +03:00
165 changed files with 33604 additions and 1897 deletions

View File

@@ -2,7 +2,7 @@ name: Dokploy Docker Build
on:
push:
branches: [main, canary, "feat/better-auth-2"]
branches: [main, canary, "1061-custom-docker-service-hostname"]
env:
IMAGE_NAME: dokploy/dokploy

22
.github/workflows/format.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: autofix.ci
on:
push:
branches: [canary]
pull_request:
branches: [canary]
jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup biomeJs
uses: biomejs/setup-biome@v2
- name: Run Biome formatter
run: biome format . --write
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@@ -61,9 +61,9 @@ pnpm install
cp apps/dokploy/.env.example apps/dokploy/.env
```
## Development
## Requirements
Is required to have **Docker** installed on your machine.
- [Docker](/GUIDES.md#docker)
### Setup

View File

@@ -29,7 +29,7 @@ WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next

49
GUIDES.md Normal file
View File

@@ -0,0 +1,49 @@
# Docker
Here's how to install docker on different operating systems:
## macOS
1. Visit [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop)
2. Download the Docker Desktop installer
3. Double-click the downloaded `.dmg` file
4. Drag Docker to your Applications folder
5. Open Docker Desktop from Applications
6. Follow the onboarding tutorial if desired
## Linux
### Ubuntu
```bash
# Update package index
sudo apt-get update
# Install prerequisites
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Set up stable repository
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
```
## Windows
1. Enable WSL2 if not already enabled
2. Visit [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop)
3. Download the installer
4. Run the installer and follow the prompts
5. Start Docker Desktop from the Start menu

View File

@@ -1006,7 +1006,7 @@ services:
volumes:
db-config-testhash:
`) as ComposeSpecification;
`);
test("Expect to change the suffix in all the possible places (4 Try)", () => {
const composeData = load(composeFileComplex) as ComposeSpecification;
@@ -1115,3 +1115,60 @@ test("Expect to change the suffix in all the possible places (5 Try)", () => {
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
});
const composeFileBackrest = `
services:
backrest:
image: garethgeorge/backrest:v1.7.3
restart: unless-stopped
ports:
- 9898
environment:
- BACKREST_PORT=9898
- BACKREST_DATA=/data
- BACKREST_CONFIG=/config/config.json
- XDG_CACHE_HOME=/cache
- TZ=\${TZ}
volumes:
- backrest/data:/data
- backrest/config:/config
- backrest/cache:/cache
- /:/userdata:ro
volumes:
backrest:
backrest-cache:
`;
const expectedDockerComposeBackrest = load(`
services:
backrest:
image: garethgeorge/backrest:v1.7.3
restart: unless-stopped
ports:
- 9898
environment:
- BACKREST_PORT=9898
- BACKREST_DATA=/data
- BACKREST_CONFIG=/config/config.json
- XDG_CACHE_HOME=/cache
- TZ=\${TZ}
volumes:
- backrest-testhash/data:/data
- backrest-testhash/config:/config
- backrest-testhash/cache:/cache
- /:/userdata:ro
volumes:
backrest-testhash:
backrest-cache-testhash:
`) as ComposeSpecification;
test("Should handle volume paths with subdirectories correctly", () => {
const composeData = load(composeFileBackrest) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
expect(updatedComposeData).toEqual(expectedDockerComposeBackrest);
});

View File

@@ -27,6 +27,12 @@ if (typeof window === "undefined") {
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
giteaBranch: "",
giteaBuildPath: "",
giteaId: "",
giteaOwner: "",
giteaRepository: "",
cleanCache: false,
watchPaths: [],
applicationStatus: "done",
appName: "",

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { Schema } from "@dokploy/server/templates";
import type { CompleteTemplate } from "@dokploy/server/templates/processors";
import { processTemplate } from "@dokploy/server/templates/processors";
import type { Schema } from "@dokploy/server/templates";
import { describe, expect, it } from "vitest";
describe("processTemplate", () => {
// Mock schema for testing
@@ -233,6 +233,49 @@ describe("processTemplate", () => {
expect(base64Value.length).toBeGreaterThanOrEqual(42);
expect(base64Value.length).toBeLessThanOrEqual(44);
});
it("should handle boolean values in env vars when provided as an array", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
domains: [],
env: [
"ENABLE_USER_SIGN_UP=false",
"DEBUG_MODE=true",
"SOME_NUMBER=42",
],
mounts: [],
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(3);
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
expect(result.envs).toContain("DEBUG_MODE=true");
expect(result.envs).toContain("SOME_NUMBER=42");
});
it("should handle boolean values in env vars when provided as an object", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
domains: [],
env: {
ENABLE_USER_SIGN_UP: false,
DEBUG_MODE: true,
SOME_NUMBER: 42,
},
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(3);
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
expect(result.envs).toContain("DEBUG_MODE=true");
expect(result.envs).toContain("SOME_NUMBER=42");
});
});
describe("mounts processing", () => {

View File

@@ -7,6 +7,12 @@ import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
giteaRepository: "",
giteaOwner: "",
giteaBranch: "",
giteaBuildPath: "",
giteaId: "",
cleanCache: false,
applicationStatus: "done",
appName: "",
autoDeploy: true,

View File

@@ -40,7 +40,7 @@ interface Props {
}
const AddRedirectchema = z.object({
replicas: z.number(),
replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string(),
});
@@ -130,9 +130,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
placeholder="1"
{...field}
onChange={(e) => {
field.onChange(Number(e.target.value));
const value = e.target.value;
field.onChange(value === "" ? 0 : Number(value));
}}
type="number"
value={field.value || ""}
/>
</FormControl>

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
@@ -32,7 +33,6 @@ 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";
const ImportSchema = z.object({
base64: z.string(),

View File

@@ -20,7 +20,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
enum BuildType {
export enum BuildType {
dockerfile = "dockerfile",
heroku_buildpacks = "heroku_buildpacks",
paketo_buildpacks = "paketo_buildpacks",
@@ -29,9 +29,18 @@ enum BuildType {
railpack = "railpack",
}
const buildTypeDisplayMap: Record<BuildType, string> = {
[BuildType.dockerfile]: "Dockerfile",
[BuildType.railpack]: "Railpack",
[BuildType.nixpacks]: "Nixpacks",
[BuildType.heroku_buildpacks]: "Heroku Buildpacks",
[BuildType.paketo_buildpacks]: "Paketo Buildpacks",
[BuildType.static]: "Static",
};
const mySchema = z.discriminatedUnion("buildType", [
z.object({
buildType: z.literal("dockerfile"),
buildType: z.literal(BuildType.dockerfile),
dockerfile: z
.string({
required_error: "Dockerfile path is required",
@@ -42,39 +51,88 @@ const mySchema = z.discriminatedUnion("buildType", [
dockerBuildStage: z.string().nullable().default(""),
}),
z.object({
buildType: z.literal("heroku_buildpacks"),
buildType: z.literal(BuildType.heroku_buildpacks),
herokuVersion: z.string().nullable().default(""),
}),
z.object({
buildType: z.literal("paketo_buildpacks"),
buildType: z.literal(BuildType.paketo_buildpacks),
}),
z.object({
buildType: z.literal("nixpacks"),
buildType: z.literal(BuildType.nixpacks),
publishDirectory: z.string().optional(),
}),
z.object({
buildType: z.literal("static"),
buildType: z.literal(BuildType.static),
}),
z.object({
buildType: z.literal("railpack"),
buildType: z.literal(BuildType.railpack),
}),
]);
type AddTemplate = z.infer<typeof mySchema>;
interface Props {
applicationId: string;
}
interface ApplicationData {
buildType: BuildType;
dockerfile?: string | null;
dockerContextPath?: string | null;
dockerBuildStage?: string | null;
herokuVersion?: string | null;
publishDirectory?: string | null;
}
function isValidBuildType(value: string): value is BuildType {
return Object.values(BuildType).includes(value as BuildType);
}
const resetData = (data: ApplicationData): AddTemplate => {
switch (data.buildType) {
case BuildType.dockerfile:
return {
buildType: BuildType.dockerfile,
dockerfile: data.dockerfile || "",
dockerContextPath: data.dockerContextPath || "",
dockerBuildStage: data.dockerBuildStage || "",
};
case BuildType.heroku_buildpacks:
return {
buildType: BuildType.heroku_buildpacks,
herokuVersion: data.herokuVersion || "",
};
case BuildType.nixpacks:
return {
buildType: BuildType.nixpacks,
publishDirectory: data.publishDirectory || undefined,
};
case BuildType.paketo_buildpacks:
return {
buildType: BuildType.paketo_buildpacks,
};
case BuildType.static:
return {
buildType: BuildType.static,
};
case BuildType.railpack:
return {
buildType: BuildType.railpack,
};
default:
const buildType = data.buildType as BuildType;
return {
buildType,
} as AddTemplate;
}
};
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } =
api.application.saveBuildType.useMutation();
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{
enabled: !!applicationId,
},
{ applicationId },
{ enabled: !!applicationId },
);
const form = useForm<AddTemplate>({
@@ -85,46 +143,36 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
});
const buildType = form.watch("buildType");
useEffect(() => {
if (data) {
if (data.buildType === "dockerfile") {
form.reset({
buildType: data.buildType,
...(data.buildType && {
dockerfile: data.dockerfile || "",
dockerContextPath: data.dockerContextPath || "",
dockerBuildStage: data.dockerBuildStage || "",
}),
});
} else if (data.buildType === "heroku_buildpacks") {
form.reset({
buildType: data.buildType,
...(data.buildType && {
herokuVersion: data.herokuVersion || "",
}),
});
} else {
form.reset({
buildType: data.buildType,
publishDirectory: data.publishDirectory || undefined,
});
}
const typedData: ApplicationData = {
...data,
buildType: isValidBuildType(data.buildType)
? (data.buildType as BuildType)
: BuildType.nixpacks, // fallback
};
form.reset(resetData(typedData));
}
}, [form.formState.isSubmitSuccessful, form.reset, data, form]);
}, [data, form]);
const onSubmit = async (data: AddTemplate) => {
await mutateAsync({
applicationId,
buildType: data.buildType,
publishDirectory:
data.buildType === "nixpacks" ? data.publishDirectory : null,
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
data.buildType === BuildType.nixpacks ? data.publishDirectory : null,
dockerfile:
data.buildType === BuildType.dockerfile ? data.dockerfile : null,
dockerContextPath:
data.buildType === "dockerfile" ? data.dockerContextPath : null,
data.buildType === BuildType.dockerfile ? data.dockerContextPath : null,
dockerBuildStage:
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
data.buildType === BuildType.dockerfile ? data.dockerBuildStage : null,
herokuVersion:
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
data.buildType === BuildType.heroku_buildpacks
? data.herokuVersion
: null,
})
.then(async () => {
toast.success("Build type saved");
@@ -160,193 +208,143 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
control={form.control}
name="buildType"
defaultValue={form.control._defaultValues.buildType}
render={({ field }) => {
return (
<FormItem className="space-y-3">
<FormLabel>Build Type</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="dockerfile" />
</FormControl>
<FormLabel className="font-normal">
Dockerfile
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="railpack" />
</FormControl>
<FormLabel className="font-normal">
Railpack{" "}
<Badge className="ml-1 text-xs px-1">New</Badge>
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="nixpacks" />
</FormControl>
<FormLabel className="font-normal">
Nixpacks
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="heroku_buildpacks" />
</FormControl>
<FormLabel className="font-normal">
Heroku Buildpacks
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="paketo_buildpacks" />
</FormControl>
<FormLabel className="font-normal">
Paketo Buildpacks
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="static" />
</FormControl>
<FormLabel className="font-normal">Static</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
);
}}
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Build Type</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-col space-y-1"
>
{Object.entries(buildTypeDisplayMap).map(
([value, label]) => (
<FormItem
key={value}
className="flex items-center space-x-3 space-y-0"
>
<FormControl>
<RadioGroupItem value={value} />
</FormControl>
<FormLabel className="font-normal">
{label}
{value === BuildType.railpack && (
<Badge className="ml-2 px-1 text-xs">New</Badge>
)}
</FormLabel>
</FormItem>
),
)}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{buildType === "heroku_buildpacks" && (
{buildType === BuildType.heroku_buildpacks && (
<FormField
control={form.control}
name="herokuVersion"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Heroku Version (Optional)</FormLabel>
<FormControl>
<Input
placeholder={"Heroku Version (Default: 24)"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
render={({ field }) => (
<FormItem>
<FormLabel>Heroku Version (Optional)</FormLabel>
<FormControl>
<Input
placeholder="Heroku Version (Default: 24)"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{buildType === "dockerfile" && (
{buildType === BuildType.dockerfile && (
<>
<FormField
control={form.control}
name="dockerfile"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Docker File</FormLabel>
<FormControl>
<Input
placeholder={"Path of your docker file"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="dockerContextPath"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Docker Context Path</FormLabel>
<FormControl>
<Input
placeholder={
"Path of your docker context default: ."
}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="dockerBuildStage"
render={({ field }) => {
return (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Docker Build Stage</FormLabel>
<FormDescription>
Allows you to target a specific stage in a
Multi-stage Dockerfile. If empty, Docker defaults to
build the last defined stage.
</FormDescription>
</div>
<FormControl>
<Input
placeholder={"E.g. production"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
</FormItem>
);
}}
/>
</>
)}
{buildType === "nixpacks" && (
<FormField
control={form.control}
name="publishDirectory"
render={({ field }) => {
return (
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Publish Directory</FormLabel>
<FormDescription>
Allows you to serve a single directory via NGINX after
the build phase. Useful if the final build assets
should be served as a static site.
</FormDescription>
</div>
<FormLabel>Docker File</FormLabel>
<FormControl>
<Input
placeholder={"Publish Directory"}
placeholder="Path of your docker file"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
)}
/>
<FormField
control={form.control}
name="dockerContextPath"
render={({ field }) => (
<FormItem>
<FormLabel>Docker Context Path</FormLabel>
<FormControl>
<Input
placeholder="Path of your docker context (default: .)"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dockerBuildStage"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Docker Build Stage</FormLabel>
<FormDescription>
Allows you to target a specific stage in a Multi-stage
Dockerfile. If empty, Docker defaults to build the
last defined stage.
</FormDescription>
</div>
<FormControl>
<Input
placeholder="E.g. production"
{...field}
value={field.value ?? ""}
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
{buildType === BuildType.nixpacks && (
<FormField
control={form.control}
name="publishDirectory"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Publish Directory</FormLabel>
<FormDescription>
Allows you to serve a single directory via NGINX after
the build phase. Useful if the final build assets should
be served as a static site.
</FormDescription>
</div>
<FormControl>
<Input
placeholder="Publish Directory"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end">

View File

@@ -41,6 +41,7 @@ import { toast } from "sonner";
import { domain } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod";
import { Dices } from "lucide-react";
import Link from "next/link";
import type z from "zod";
type Domain = z.infer<typeof domain>;
@@ -83,6 +84,13 @@ export const AddDomain = ({
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const { data: canGenerateTraefikMeDomains } =
api.domain.canGenerateTraefikMeDomains.useQuery({
serverId: application?.serverId || "",
});
console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains);
const form = useForm<Domain>({
resolver: zodResolver(domain),
defaultValues: {
@@ -186,6 +194,21 @@ export const AddDomain = ({
name="host"
render={({ field }) => (
<FormItem>
{!canGenerateTraefikMeDomains &&
field.value.includes("traefik.me") && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{application?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to make your traefik.me domain work.
</AlertBlock>
)}
<FormLabel>Host</FormLabel>
<div className="flex gap-2">
<FormControl>

View File

@@ -4,10 +4,10 @@ import { Form } from "@/components/ui/form";
import { Secrets } from "@/components/ui/secrets";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { useEffect } from "react";
const addEnvironmentSchema = z.object({
env: z.string(),

View File

@@ -1,4 +1,6 @@
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } 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 { Badge } from "@/components/ui/badge";
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const BitbucketProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -115,7 +115,11 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Username" autoComplete="username" {...field} />
<Input
placeholder="Username"
autoComplete="username"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -130,7 +134,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input placeholder="Password" autoComplete="one-time-code" {...field} type="password" />
<Input
placeholder="Password"
autoComplete="one-time-code"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -26,15 +26,15 @@ import {
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import { useRouter } from "next/router";
import Link from "next/link";
import { useRouter } from "next/router";
import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { GitIcon } from "@/components/icons/data-tools-icons";
const GitProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -0,0 +1,515 @@
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
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 { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface GiteaRepository {
name: string;
url: string;
id: number;
owner: {
username: string;
};
}
interface GiteaBranch {
name: string;
commit: {
id: string;
};
}
const GiteaProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
interface Props {
applicationId: string;
}
export const SaveGiteaProvider = ({ applicationId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGiteaProvider } =
api.application.saveGiteaProvider.useMutation();
const form = useForm<GiteaProvider>({
defaultValues: {
buildPath: "/",
repository: {
owner: "",
repo: "",
},
giteaId: "",
branch: "",
watchPaths: [],
},
resolver: zodResolver(GiteaProviderSchema),
});
const repository = form.watch("repository");
const giteaId = form.watch("giteaId");
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
{ giteaId },
{
enabled: !!giteaId,
},
);
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
} = api.gitea.getGiteaRepositories.useQuery(
{
giteaId,
},
{
enabled: !!giteaId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.gitea.getGiteaBranches.useQuery(
{
owner: repository?.owner,
repositoryName: repository?.repo,
giteaId: giteaId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.giteaBranch || "",
repository: {
repo: data.giteaRepository || "",
owner: data.giteaOwner || "",
},
buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
giteaBranch: data.branch,
giteaRepository: data.repository.repo,
giteaOwner: data.repository.owner,
giteaBuildPath: data.buildPath,
giteaId: data.giteaId,
applicationId,
watchPaths: data.watchPaths,
})
.then(async () => {
toast.success("Service Provider Saved");
await refetch();
})
.catch(() => {
toast.error("Error saving the Gitea provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="giteaId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Gitea Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Gitea Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{giteaProviders?.map((giteaProvider) => (
<SelectItem
key={giteaProvider.giteaId}
value={giteaProvider.giteaId}
>
{giteaProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<div className="flex items-center justify-between">
<FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && (
<Link
href={`${giteaUrl}/${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"
>
<GiteaIcon className="h-4 w-4" />
<span>View Repository</span>
</Link>
)}
</div>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo: GiteaRepository) =>
repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories && repositories.length === 0 && (
<CommandEmpty>
No repositories found.
</CommandEmpty>
)}
{repositories?.map((repo: GiteaRepository) => {
return (
<CommandItem
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
});
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
);
})}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch: GiteaBranch) =>
branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
)}
{!repository?.owner && (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a repository
</span>
)}
<ScrollArea className="h-96">
<CommandEmpty>No branch found.</CommandEmpty>
<CommandGroup>
{branches?.map((branch: GiteaBranch) => (
<CommandItem
value={branch.name}
key={branch.commit.id}
onSelect={() => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
<FormMessage />
</Popover>
</FormItem>
)}
/>
<FormField
control={form.control}
name="buildPath"
render={({ field }) => (
<FormItem>
<FormLabel>Build Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path: string, index: number) => (
<Badge
key={index}
variant="secondary"
className="flex items-center gap-1"
>
{path}
<X
className="size-3 cursor-pointer hover:text-destructive"
onClick={() => {
const newPaths = [...field.value];
newPaths.splice(index, 1);
field.onChange(newPaths);
}}
/>
</Badge>
))}
</div>
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const path = input.value.trim();
if (path) {
field.onChange([...field.value, path]);
input.value = "";
}
}
}}
/>
</FormControl>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const input = document.querySelector(
'input[placeholder*="Enter a path"]',
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
field.onChange([...field.value, path]);
input.value = "";
}
}}
>
<Plus className="size-4" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingGiteaProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

@@ -1,3 +1,5 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -34,17 +36,15 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
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 { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import Link from "next/link";
import { GithubIcon } from "@/components/icons/data-tools-icons";
const GithubProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -1,4 +1,6 @@
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -35,17 +37,15 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
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 { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import Link from "next/link";
import { GitlabIcon } from "@/components/icons/data-tools-icons";
const GitlabProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -1,10 +1,12 @@
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
import {
BitbucketIcon,
DockerIcon,
GitIcon,
GiteaIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
@@ -18,7 +20,14 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider";
import { SaveDragNDrop } from "./save-drag-n-drop";
import { SaveGitlabProvider } from "./save-gitlab-provider";
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
type TabState =
| "github"
| "docker"
| "git"
| "drop"
| "gitlab"
| "bitbucket"
| "gitea";
interface Props {
applicationId: string;
@@ -29,6 +38,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data: bitbucketProviders } =
api.bitbucket.bitbucketProviders.useQuery();
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data: application } = api.application.one.useQuery({ applicationId });
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
@@ -78,6 +88,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
<BitbucketIcon className="size-4 text-current fill-current" />
Bitbucket
</TabsTrigger>
<TabsTrigger
value="gitea"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GiteaIcon className="size-4 text-current fill-current" />
Gitea
</TabsTrigger>
<TabsTrigger
value="docker"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
@@ -162,6 +179,26 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
</div>
)}
</TabsContent>
<TabsContent value="gitea" className="w-full p-2">
{giteaProviders && giteaProviders?.length > 0 ? (
<SaveGiteaProvider applicationId={applicationId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GiteaIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using Gitea, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="docker" className="w-full p-2">
<SaveDockerProvider applicationId={applicationId} />
</TabsContent>

View File

@@ -10,14 +10,14 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Ban,
CheckCircle2,
Hammer,
HelpCircle,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useRouter } from "next/router";
@@ -55,7 +55,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
<DialogAction
title="Deploy Application"
description="Are you sure you want to deploy this application?"
@@ -79,12 +79,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -114,9 +116,24 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
});
}}
>
<Button variant="secondary" isLoading={isReloading}>
Reload
<RefreshCcw className="size-4" />
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Reload the application without rebuilding it</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
<DialogAction
@@ -139,13 +156,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button
variant="secondary"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Rebuild
<Hammer className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Hammer className="size-4 mr-1" />
Rebuild
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -180,13 +198,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -219,13 +238,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -241,15 +261,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Terminal className="size-4 mr-1" />
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"
aria-label="Toggle autodeploy"
checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => {
await update({
@@ -264,14 +287,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.error("Error updating Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/>
</div>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Clean Cache</span>
<Switch
aria-label="Toggle italic"
aria-label="Toggle clean cache"
checked={data?.cleanCache || false}
onCheckedChange={async (enabled) => {
await update({
@@ -286,7 +309,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.error("Error updating Clean Cache");
});
}}
className="flex flex-row gap-2 items-center"
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/>
</div>
</CardContent>

View File

@@ -41,6 +41,7 @@ import {
import { domainCompose } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import type z from "zod";
type Domain = z.infer<typeof domainCompose>;
@@ -102,6 +103,11 @@ export const AddDomainCompose = ({
? api.domain.update.useMutation()
: api.domain.create.useMutation();
const { data: canGenerateTraefikMeDomains } =
api.domain.canGenerateTraefikMeDomains.useQuery({
serverId: compose?.serverId || "",
});
const form = useForm<Domain>({
resolver: zodResolver(domainCompose),
defaultValues: {
@@ -313,6 +319,21 @@ export const AddDomainCompose = ({
name="host"
render={({ field }) => (
<FormItem>
{!canGenerateTraefikMeDomains &&
field.value.includes("traefik.me") && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{compose?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to make your traefik.me domain work.
</AlertBlock>
)}
<FormLabel>Host</FormLabel>
<div className="flex gap-2">
<FormControl>

View File

@@ -7,9 +7,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
@@ -34,7 +34,7 @@ export const ComposeActions = ({ composeId }: Props) => {
api.compose.stop.useMutation();
return (
<div className="flex flex-row gap-4 w-full flex-wrap ">
<TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
<DialogAction
title="Deploy Compose"
description="Are you sure you want to deploy this compose?"
@@ -58,12 +58,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button
variant="default"
isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -74,36 +76,37 @@ export const ComposeActions = ({ composeId }: Props) => {
</Button>
</DialogAction>
<DialogAction
title="Rebuild Compose"
description="Are you sure you want to rebuild this compose?"
title="Reload Compose"
description="Are you sure you want to reload this compose?"
type="default"
onClick={async () => {
await redeploy({
composeId: composeId,
})
.then(() => {
toast.success("Compose rebuilt successfully");
toast.success("Compose reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error rebuilding compose");
toast.error("Error reloading compose");
});
}}
>
<Button
variant="secondary"
isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Rebuild
<Hammer className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Only rebuilds the compose without downloading new code</p>
<p>Reload the compose without rebuilding it</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
@@ -131,13 +134,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -169,13 +173,14 @@ export const ComposeActions = ({ composeId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -191,15 +196,18 @@ export const ComposeActions = ({ composeId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Terminal className="size-4 mr-1" />
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"
aria-label="Toggle autodeploy"
checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => {
await update({
@@ -214,7 +222,7 @@ export const ComposeActions = ({ composeId }: Props) => {
toast.error("Error updating Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
/>
</div>
</div>

View File

@@ -1,4 +1,6 @@
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } 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 { Badge } from "@/components/ui/badge";
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const BitbucketProviderSchema = z.object({
composePath: z.string().min(1),

View File

@@ -1,3 +1,4 @@
import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -27,13 +28,12 @@ import {
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GitIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const GitProviderSchema = z.object({
composePath: z.string().min(1),

View File

@@ -0,0 +1,483 @@
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import type { Repository } from "@/utils/gitea-utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, Plus, X } 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";
const GiteaProviderSchema = z.object({
composePath: z.string().min(1),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
interface Props {
composeId: string;
}
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingGiteaProvider } =
api.compose.update.useMutation();
const form = useForm<GiteaProvider>({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
owner: "",
repo: "",
},
giteaId: "",
branch: "",
watchPaths: [],
},
resolver: zodResolver(GiteaProviderSchema),
});
const repository = form.watch("repository");
const giteaId = form.watch("giteaId");
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
{ giteaId },
{
enabled: !!giteaId,
},
);
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
} = api.gitea.getGiteaRepositories.useQuery<Repository[]>(
{
giteaId,
},
{
enabled: !!giteaId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.gitea.getGiteaBranches.useQuery(
{
owner: repository?.owner,
repositoryName: repository?.repo,
giteaId: giteaId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.giteaBranch || "",
repository: {
repo: data.giteaRepository || "",
owner: data.giteaOwner || "",
},
composePath: data.composePath || "./docker-compose.yml",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
giteaBranch: data.branch,
giteaRepository: data.repository.repo,
giteaOwner: data.repository.owner,
composePath: data.composePath,
giteaId: data.giteaId,
composeId,
sourceType: "gitea",
composeStatus: "idle",
watchPaths: data.watchPaths,
} as any)
.then(async () => {
toast.success("Service Provider Saved");
await refetch();
})
.catch(() => {
toast.error("Error saving the Gitea provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="giteaId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Gitea Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Gitea Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{giteaProviders?.map((giteaProvider) => (
<SelectItem
key={giteaProvider.giteaId}
value={giteaProvider.giteaId}
>
{giteaProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<div className="flex items-center justify-between">
<FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && (
<Link
href={`${giteaUrl}/${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"
>
<GiteaIcon className="h-4 w-4" />
<span>View Repository</span>
</Link>
)}
</div>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
key={repo.url}
value={repo.name}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username,
repo: repo.name,
});
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch) => branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branches..."
className="h-9"
/>
<CommandEmpty>No branches found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{branches?.map((branch) => (
<CommandItem
key={branch.name}
value={branch.name}
onSelect={() =>
form.setValue("branch", branch.name)
}
>
<span className="flex items-center gap-2">
{branch.name}
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.branch && (
<p className={cn("text-sm font-medium text-destructive")}>
Branch is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="composePath"
render={({ field }) => (
<FormItem>
<FormLabel>Compose Path</FormLabel>
<FormControl>
<Input placeholder="docker-compose.yml" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
input.value = "";
}
}
}}
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const input = document.querySelector(
'input[placeholder*="Enter a path"]',
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
field.onChange([...(field.value || []), path]);
input.value = "";
}
}}
>
<Plus className="size-4" />
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end">
<Button type="submit" isLoading={isSavingGiteaProvider}>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

@@ -1,3 +1,4 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -39,12 +40,11 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } 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 { GithubIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const GithubProviderSchema = z.object({
composePath: z.string().min(1),

View File

@@ -1,4 +1,6 @@
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } 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 { Badge } from "@/components/ui/badge";
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const GitlabProviderSchema = z.object({
composePath: z.string().min(1),

View File

@@ -1,6 +1,7 @@
import {
BitbucketIcon,
GitIcon,
GiteaIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
@@ -14,10 +15,11 @@ import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
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";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
interface Props {
composeId: string;
}
@@ -27,9 +29,11 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data: bitbucketProviders } =
api.bitbucket.bitbucketProviders.useQuery();
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data: compose } = api.compose.one.useQuery({ composeId });
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
return (
<Card className="group relative w-full bg-transparent">
<CardHeader>
@@ -54,21 +58,21 @@ 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-5 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<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">
<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"
>
<GithubIcon className="size-4 text-current fill-current" />
Github
GitHub
</TabsTrigger>
<TabsTrigger
value="gitlab"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GitlabIcon className="size-4 text-current fill-current" />
Gitlab
GitLab
</TabsTrigger>
<TabsTrigger
value="bitbucket"
@@ -77,7 +81,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
<BitbucketIcon className="size-4 text-current fill-current" />
Bitbucket
</TabsTrigger>
<TabsTrigger
value="gitea"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GiteaIcon className="size-4 text-current fill-current" /> Gitea
</TabsTrigger>
<TabsTrigger
value="git"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
@@ -89,11 +98,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
value="raw"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<CodeIcon className="size-4 " />
<CodeIcon className="size-4" />
Raw
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="github" className="w-full p-2">
{githubProviders && githubProviders?.length > 0 ? (
<SaveGithubProviderCompose composeId={composeId} />
@@ -154,6 +164,26 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
</div>
)}
</TabsContent>
<TabsContent value="gitea" className="w-full p-2">
{giteaProviders && giteaProviders?.length > 0 ? (
<SaveGiteaProviderCompose composeId={composeId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GiteaIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using Gitea, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="git" className="w-full p-2">
<SaveGitProviderCompose composeId={composeId} />
</TabsContent>

View File

@@ -147,7 +147,9 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Enable Isolated Deployment ({data?.appName})</FormLabel>
<FormLabel>
Enable Isolated Deployment ({data?.appName})
</FormLabel>
<FormDescription>
Enable isolated deployment to the compose file.
</FormDescription>

View File

@@ -62,6 +62,11 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<AlertBlock type="info">
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
</AlertBlock>
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"

View File

@@ -61,7 +61,7 @@ type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
interface Props {
databaseId: string;
databaseType: "postgres" | "mariadb" | "mysql" | "mongo";
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
refetch: () => void;
}
@@ -85,7 +85,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
useEffect(() => {
form.reset({
database: "",
database: databaseType === "web-server" ? "dokploy" : "",
destinationId: "",
enabled: true,
prefix: "/",
@@ -112,7 +112,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
? {
mongoId: databaseId,
}
: undefined;
: databaseType === "web-server"
? {
userId: databaseId,
}
: undefined;
await createBackup({
destinationId: data.destinationId,
@@ -236,7 +240,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
<FormItem>
<FormLabel>Database</FormLabel>
<FormControl>
<Input placeholder={"dokploy"} {...field} />
<Input
disabled={databaseType === "web-server"}
placeholder={"dokploy"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -286,16 +294,21 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
<FormItem>
<FormLabel>Keep the latest</FormLabel>
<FormControl>
<Input type="number" placeholder={"keeps all the backups if left empty"} {...field} />
<Input
type="number"
placeholder={"keeps all the backups if left empty"}
{...field}
/>
</FormControl>
<FormDescription>
Optional. If provided, only keeps the latest N backups in the cloud.
Optional. If provided, only keeps the latest N backups
in the cloud.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
/>
<FormField
control={form.control}
name="enabled"

View File

@@ -0,0 +1,379 @@
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { debounce } from "lodash";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { ServiceType } from "../../application/advanced/show-resources";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
interface Props {
databaseId: string;
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
serverId?: string | null;
}
const RestoreBackupSchema = z.object({
destinationId: z
.string({
required_error: "Please select a destination",
})
.min(1, {
message: "Destination is required",
}),
backupFile: z
.string({
required_error: "Please select a backup file",
})
.min(1, {
message: "Backup file is required",
}),
databaseName: z
.string({
required_error: "Please enter a database name",
})
.min(1, {
message: "Database name is required",
}),
});
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
export const RestoreBackup = ({
databaseId,
databaseType,
serverId,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState("");
const { data: destinations = [] } = api.destination.all.useQuery();
const form = useForm<RestoreBackup>({
defaultValues: {
destinationId: "",
backupFile: "",
databaseName: databaseType === "web-server" ? "dokploy" : "",
},
resolver: zodResolver(RestoreBackupSchema),
});
const destionationId = form.watch("destinationId");
const debouncedSetSearch = debounce((value: string) => {
setSearch(value);
}, 300);
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
{
destinationId: destionationId,
search,
serverId: serverId ?? "",
},
{
enabled: isOpen && !!destionationId,
},
);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
// const { mutateAsync: restore, isLoading: isRestoring } =
// api.backup.restoreBackup.useMutation();
api.backup.restoreBackupWithLogs.useSubscription(
{
databaseId,
databaseType,
databaseName: form.watch("databaseName"),
backupFile: form.watch("backupFile"),
destinationId: form.watch("destinationId"),
},
{
enabled: isDeploying,
onData(log) {
if (!isDrawerOpen) {
setIsDrawerOpen(true);
}
if (log === "Restore completed successfully!") {
setIsDeploying(false);
}
const parsedLogs = parseLogs(log);
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
},
onError(error) {
console.error("Restore logs error:", error);
setIsDeploying(false);
},
},
);
const onSubmit = async (_data: RestoreBackup) => {
setIsDeploying(true);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">
<RotateCcw className="mr-2 size-4" />
Restore Backup
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center">
<RotateCcw className="mr-2 size-4" />
Restore Backup
</DialogTitle>
<DialogDescription>
Select a destination and search for backup files
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
id="hook-form-restore-backup"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="destinationId"
render={({ field }) => (
<FormItem className="">
<FormLabel>Destination</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{field.value
? destinations.find(
(d) => d.destinationId === field.value,
)?.name
: "Select Destination"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search destinations..."
className="h-9"
/>
<CommandEmpty>No destinations found.</CommandEmpty>
<ScrollArea className="h-64">
<CommandGroup>
{destinations.map((destination) => (
<CommandItem
value={destination.destinationId}
key={destination.destinationId}
onSelect={() => {
form.setValue(
"destinationId",
destination.destinationId,
);
}}
>
{destination.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
destination.destinationId === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="backupFile"
render={({ field }) => (
<FormItem className="">
<FormLabel className="flex items-center justify-between">
Search Backup Files
{field.value && (
<Badge variant="outline">
{field.value}
<Copy
className="ml-2 size-4 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
copy(field.value);
toast.success("Backup file copied to clipboard");
}}
/>
</Badge>
)}
</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{field.value || "Search and select a backup file"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search backup files..."
onValueChange={debouncedSetSearch}
className="h-9"
/>
{isLoading ? (
<div className="py-6 text-center text-sm">
Loading backup files...
</div>
) : files.length === 0 && search ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No backup files found for "{search}"
</div>
) : files.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
No backup files available
</div>
) : (
<ScrollArea className="h-64">
<CommandGroup>
{files.map((file) => (
<CommandItem
value={file}
key={file}
onSelect={() => {
form.setValue("backupFile", file);
}}
>
<div className="flex w-full justify-between">
<span>{file}</span>
</div>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
)}
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="databaseName"
render={({ field }) => (
<FormItem className="">
<FormLabel>Database Name</FormLabel>
<FormControl>
<Input
disabled={databaseType === "web-server"}
{...field}
placeholder="Enter database name"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isDeploying}
form="hook-form-restore-backup"
type="submit"
disabled={!form.watch("backupFile")}
>
Restore
</Button>
</DialogFooter>
</form>
</Form>
<DrawerLogs
isOpen={isDrawerOpen}
onClose={() => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
// refetch();
}}
filteredLogs={filteredLogs}
/>
</DialogContent>
</Dialog>
);
};

View File

@@ -14,20 +14,23 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources";
import { AddBackup } from "./add-backup";
import { RestoreBackup } from "./restore-backup";
import { UpdateBackup } from "./update-backup";
import { useState } from "react";
interface Props {
id: string;
type: Exclude<ServiceType, "application" | "redis">;
type: Exclude<ServiceType, "application" | "redis"> | "web-server";
}
export const ShowBackups = ({ id, type }: Props) => {
const [activeManualBackup, setActiveManualBackup] = useState<string | undefined>();
const [activeManualBackup, setActiveManualBackup] = useState<
string | undefined
>();
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
@@ -35,6 +38,7 @@ export const ShowBackups = ({ id, type }: Props) => {
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
"web-server": () => api.user.getBackups.useQuery(),
};
const { data } = api.destination.all.useQuery();
const { data: postgres, refetch } = queryMap[type]
@@ -46,6 +50,7 @@ export const ShowBackups = ({ id, type }: Props) => {
mysql: () => api.backup.manualBackupMySql.useMutation(),
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
mongo: () => api.backup.manualBackupMongo.useMutation(),
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
};
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
@@ -61,7 +66,10 @@ export const ShowBackups = ({ id, type }: Props) => {
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
<div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle>
<CardTitle className="text-xl flex flex-row gap-2">
<Database className="size-6 text-muted-foreground" />
Backups
</CardTitle>
<CardDescription>
Add backups to your database to save the data to a different
provider.
@@ -69,7 +77,20 @@ export const ShowBackups = ({ id, type }: Props) => {
</div>
{postgres && postgres?.backups?.length > 0 && (
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
{type !== "web-server" && (
<AddBackup
databaseId={id}
databaseType={type}
refetch={refetch}
/>
)}
<RestoreBackup
databaseId={id}
databaseType={type}
serverId={"serverId" in postgres ? postgres.serverId : undefined}
/>
</div>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
@@ -96,11 +117,20 @@ export const ShowBackups = ({ id, type }: Props) => {
<span className="text-base text-muted-foreground">
No backups configured
</span>
<AddBackup
databaseId={id}
databaseType={type}
refetch={refetch}
/>
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<AddBackup
databaseId={id}
databaseType={type}
refetch={refetch}
/>
<RestoreBackup
databaseId={id}
databaseType={type}
serverId={
"serverId" in postgres ? postgres.serverId : undefined
}
/>
</div>
</div>
) : (
<div className="flex flex-col pt-2">
@@ -142,7 +172,7 @@ export const ShowBackups = ({ id, type }: Props) => {
<div className="flex flex-col gap-1">
<span className="font-medium">Keep Latest</span>
<span className="text-sm text-muted-foreground">
{backup.keepLatestCount || 'All'}
{backup.keepLatestCount || "All"}
</span>
</div>
</div>
@@ -153,7 +183,10 @@ export const ShowBackups = ({ id, type }: Props) => {
<Button
type="button"
variant="ghost"
isLoading={isManualBackup && activeManualBackup === backup.backupId}
isLoading={
isManualBackup &&
activeManualBackup === backup.backupId
}
onClick={async () => {
setActiveManualBackup(backup.backupId);
await manualBackup({
@@ -178,6 +211,7 @@ export const ShowBackups = ({ id, type }: Props) => {
<TooltipContent>Run Manual Backup</TooltipContent>
</Tooltip>
</TooltipProvider>
<UpdateBackup
backupId={backup.backupId}
refetch={refetch}

View File

@@ -92,7 +92,9 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
enabled: backup.enabled || false,
prefix: backup.prefix,
schedule: backup.schedule,
keepLatestCount: backup.keepLatestCount ? Number(backup.keepLatestCount) : undefined,
keepLatestCount: backup.keepLatestCount
? Number(backup.keepLatestCount)
: undefined,
});
}
}, [form, form.reset, backup]);
@@ -274,10 +276,15 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
<FormItem>
<FormLabel>Keep the latest</FormLabel>
<FormControl>
<Input type="number" placeholder={"keeps all the backups if left empty"} {...field} />
<Input
type="number"
placeholder={"keeps all the backups if left empty"}
{...field}
/>
</FormControl>
<FormDescription>
Optional. If provided, only keeps the latest N backups in the cloud.
Optional. If provided, only keeps the latest N backups
in the cloud.
</FormDescription>
<FormMessage />
</FormItem>

View File

@@ -119,7 +119,6 @@ export const DockerLogsId: React.FC<Props> = ({
const wsUrl = `${protocol}//${
window.location.host
}/docker-container-logs?${params.toString()}`;
console.log("Connecting to WebSocket:", wsUrl);
const ws = new WebSocket(wsUrl);
const resetNoDataTimeout = () => {
@@ -136,7 +135,6 @@ export const DockerLogsId: React.FC<Props> = ({
ws.close();
return;
}
console.log("WebSocket connected");
resetNoDataTimeout();
};

View File

@@ -1,6 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
@@ -20,11 +20,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
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 Link from "next/link";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
@@ -111,7 +111,10 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}

View File

@@ -8,15 +8,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
@@ -92,12 +86,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -107,6 +103,8 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip>
</Button>
</DialogAction>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<DialogAction
title="Reload Mariadb"
description="Are you sure you want to reload this mariadb?"
@@ -128,13 +126,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -144,7 +143,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip>
</Button>
</DialogAction>
{data?.applicationStatus === "idle" ? (
</TooltipProvider>
{data?.applicationStatus === "idle" ? (
<TooltipProvider delayDuration={0}>
<DialogAction
title="Start Mariadb"
description="Are you sure you want to start this mariadb?"
@@ -165,13 +166,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -184,7 +186,9 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip>
</Button>
</DialogAction>
) : (
</TooltipProvider>
) : (
<TooltipProvider delayDuration={0}>
<DialogAction
title="Stop Mariadb"
description="Are you sure you want to stop this mariadb?"
@@ -204,13 +208,14 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -220,15 +225,29 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
</Tooltip>
</Button>
</DialogAction>
)}
</TooltipProvider>
</TooltipProvider>
)}
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MariaDB container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -1,6 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
@@ -20,11 +20,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
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 Link from "next/link";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
@@ -111,7 +111,10 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}

View File

@@ -8,15 +8,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
@@ -91,12 +85,14 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -127,13 +123,14 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -164,13 +161,14 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -203,13 +201,14 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -225,9 +224,23 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MongoDB container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -1,6 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
@@ -20,11 +20,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
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 Link from "next/link";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
@@ -111,7 +111,10 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}

View File

@@ -8,15 +8,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
@@ -77,7 +71,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<DialogAction
title="Deploy Mysql"
title="Deploy MySQL"
description="Are you sure you want to deploy this mysql?"
type="default"
onClick={async () => {
@@ -89,12 +83,14 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -105,7 +101,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</Button>
</DialogAction>
<DialogAction
title="Reload Mysql"
title="Reload MySQL"
description="Are you sure you want to reload this mysql?"
type="default"
onClick={async () => {
@@ -114,24 +110,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
appName: data?.appName || "",
})
.then(() => {
toast.success("Mysql reloaded successfully");
toast.success("MySQL reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Mysql");
toast.error("Error reloading MySQL");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -143,7 +140,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Mysql"
title="Start MySQL"
description="Are you sure you want to start this mysql?"
type="default"
onClick={async () => {
@@ -151,24 +148,25 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
mysqlId: mysqlId,
})
.then(() => {
toast.success("Mysql started successfully");
toast.success("MySQL started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Mysql");
toast.error("Error starting MySQL");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -183,31 +181,32 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</DialogAction>
) : (
<DialogAction
title="Stop Mysql"
title="Stop MySQL"
description="Are you sure you want to stop this mysql?"
onClick={async () => {
await stop({
mysqlId: mysqlId,
})
.then(() => {
toast.success("Mysql stopped successfully");
toast.success("MySQL stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Mysql");
toast.error("Error stopping MySQL");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -223,9 +222,23 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the MySQL container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -1,6 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
@@ -20,11 +20,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
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 Link from "next/link";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
@@ -113,7 +113,10 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}

View File

@@ -8,15 +8,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
@@ -78,9 +72,9 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0}>
<TooltipProvider disableHoverableContent={false}>
<DialogAction
title="Deploy Postgres"
title="Deploy PostgreSQL"
description="Are you sure you want to deploy this postgres?"
type="default"
onClick={async () => {
@@ -92,12 +86,14 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -108,7 +104,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</Button>
</DialogAction>
<DialogAction
title="Reload Postgres"
title="Reload PostgreSQL"
description="Are you sure you want to reload this postgres?"
type="default"
onClick={async () => {
@@ -117,24 +113,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
appName: data?.appName || "",
})
.then(() => {
toast.success("Postgres reloaded successfully");
toast.success("PostgreSQL reloaded successfully");
refetch();
})
.catch(() => {
toast.error("Error reloading Postgres");
toast.error("Error reloading PostgreSQL");
});
}}
>
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -146,7 +143,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</DialogAction>
{data?.applicationStatus === "idle" ? (
<DialogAction
title="Start Postgres"
title="Start PostgreSQL"
description="Are you sure you want to start this postgres?"
type="default"
onClick={async () => {
@@ -154,24 +151,25 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
postgresId: postgresId,
})
.then(() => {
toast.success("Postgres started successfully");
toast.success("PostgreSQL started successfully");
refetch();
})
.catch(() => {
toast.error("Error starting Postgres");
toast.error("Error starting PostgreSQL");
});
}}
>
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -186,31 +184,32 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</DialogAction>
) : (
<DialogAction
title="Stop Postgres"
title="Stop PostgreSQL"
description="Are you sure you want to stop this postgres?"
onClick={async () => {
await stop({
postgresId: postgresId,
})
.then(() => {
toast.success("Postgres stopped successfully");
toast.success("PostgreSQL stopped successfully");
refetch();
})
.catch(() => {
toast.error("Error stopping Postgres");
toast.error("Error stopping PostgreSQL");
});
}}
>
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -226,9 +225,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the PostgreSQL container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { PenBox } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -94,9 +94,9 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
<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">
@@ -151,6 +151,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
isLoading={isLoading}
form="hook-form-update-postgres"
type="submit"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Update
</Button>

View File

@@ -67,7 +67,7 @@ import {
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";

View File

@@ -13,7 +13,6 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { useState } from "react";
const examples = [
"Make a personal blog",
@@ -23,7 +22,7 @@ const examples = [
"Sendgrid service opensource analogue",
];
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery();

View File

@@ -0,0 +1,172 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
export type Services = {
appName: string;
serverId?: string | null;
name: string;
type:
| "mariadb"
| "application"
| "postgres"
| "mysql"
| "mongo"
| "redis"
| "compose";
description?: string | null;
id: string;
createdAt: string;
status?: "idle" | "running" | "done" | "error";
};
interface DuplicateProjectProps {
projectId: string;
services: Services[];
selectedServiceIds: string[];
}
export const DuplicateProject = ({
projectId,
services,
selectedServiceIds,
}: DuplicateProjectProps) => {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const utils = api.useUtils();
const router = useRouter();
const selectedServices = services.filter((service) =>
selectedServiceIds.includes(service.id),
);
const { mutateAsync: duplicateProject, isLoading } =
api.project.duplicate.useMutation({
onSuccess: async (newProject) => {
await utils.project.all.invalidate();
toast.success("Project duplicated successfully");
setOpen(false);
router.push(`/dashboard/project/${newProject.projectId}`);
},
onError: (error) => {
toast.error(error.message);
},
});
const handleDuplicate = async () => {
if (!name) {
toast.error("Project name is required");
return;
}
await duplicateProject({
sourceProjectId: projectId,
name,
description,
includeServices: true,
selectedServices: selectedServices.map((service) => ({
id: service.id,
type: service.type,
})),
});
};
return (
<Dialog
open={open}
onOpenChange={(isOpen) => {
setOpen(isOpen);
if (!isOpen) {
// Reset form when closing
setName("");
setDescription("");
}
}}
>
<DialogTrigger asChild>
<Button variant="ghost" className="w-full justify-start">
<Copy className="mr-2 h-4 w-4" />
Duplicate
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle>
<DialogDescription>
Create a new project with the selected services
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New project name"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
<div className="grid gap-2">
<Label>Selected services to duplicate</Label>
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
{selectedServices.map((service) => (
<div key={service.id} className="flex items-center space-x-2">
<span className="text-sm">
{service.name} ({service.type})
</span>
</div>
))}
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setOpen(false)}
disabled={isLoading}
>
Cancel
</Button>
<Button onClick={handleDuplicate} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Duplicating...
</>
) : (
"Duplicate"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -186,7 +186,9 @@ export const ShowProjects = () => {
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span className="truncate">{domain.host}</span>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
@@ -222,7 +224,9 @@ export const ShowProjects = () => {
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span className="truncate">{domain.host}</span>
<span className="truncate">
{domain.host}
</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>

View File

@@ -1,6 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
@@ -20,11 +20,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
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 Link from "next/link";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
@@ -105,7 +105,10 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
<Link
href="/dashboard/settings/server"
className="text-primary"
>
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}

View File

@@ -8,15 +8,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api";
import {
Ban,
CheckCircle2,
HelpCircle,
RefreshCcw,
Terminal,
} from "lucide-react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
@@ -91,12 +85,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button
variant="default"
isLoading={data?.applicationStatus === "running"}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Deploy
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Rocket className="size-4 mr-1" />
Deploy
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -127,13 +123,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button
variant="secondary"
isLoading={isReloading}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Reload
<RefreshCcw className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<RefreshCcw className="size-4 mr-1" />
Reload
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -164,13 +161,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button
variant="secondary"
isLoading={isStarting}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Start
<CheckCircle2 className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<CheckCircle2 className="size-4 mr-1" />
Start
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -203,13 +201,14 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
<Button
variant="destructive"
isLoading={isStopping}
className="flex items-center gap-1.5"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
Stop
<Ban className="size-4" />
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
<div className="flex items-center">
<Ban className="size-4 mr-1" />
Stop
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
@@ -225,9 +224,23 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
>
<Button variant="outline">
<Terminal />
Open Terminal
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Terminal className="size-4 mr-1" />
Open Terminal
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>Open a terminal to the Redis container</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DockerTerminalModal>
</CardContent>

View File

@@ -1,10 +1,10 @@
import { api } from "@/utils/api";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { api } from "@/utils/api";
import {
Area,
AreaChart,

View File

@@ -25,13 +25,13 @@ import {
import { type RouterOutputs, api } from "@/utils/api";
import { format } from "date-fns";
import {
ArrowDownUp,
AlertCircle,
InfoIcon,
ArrowDownUp,
Calendar as CalendarIcon,
InfoIcon,
} from "lucide-react";
import Link from "next/link";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { RequestDistributionChart } from "./request-distribution-chart";
import { RequestsTable } from "./requests-table";

View File

@@ -1,14 +1,22 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogDescription,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
@@ -17,22 +25,14 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import { z } from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import copy from "copy-to-clipboard";
import { CodeEditor } from "@/components/shared/code-editor";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),

View File

@@ -1,3 +1,5 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -7,13 +9,11 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } from "lucide-react";
import { formatDistanceToNow } from "date-fns";
import { Clock, ExternalLinkIcon, KeyIcon, Tag, Trash2 } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { formatDistanceToNow } from "date-fns";
import { DialogAction } from "@/components/shared/dialog-action";
import { AddApiKey } from "./add-api-key";
import { Badge } from "@/components/ui/badge";
export const ShowApiKeys = () => {
const { data, refetch } = api.user.get.useQuery();

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -48,6 +49,10 @@ export const AddNode = ({ serverId }: Props) => {
Architecture
<ExternalLink className="h-4 w-4" />
</Link>
<AlertBlock type="warning">
Make sure you use the same architecture as the node you are
adding.
</AlertBlock>
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
@@ -56,10 +61,10 @@ export const AddNode = ({ serverId }: Props) => {
<TabsTrigger value="worker">Worker</TabsTrigger>
<TabsTrigger value="manager">Manager</TabsTrigger>
</TabsList>
<TabsContent value="worker" className="pt-4">
<TabsContent value="worker" className="pt-4 overflow-hidden">
<AddWorker serverId={serverId} />
</TabsContent>
<TabsContent value="manager" className="pt-4">
<TabsContent value="manager" className="pt-4 overflow-hidden">
<AddManager serverId={serverId} />
</TabsContent>
</Tabs>

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CardContent } from "@/components/ui/card";
import {
DialogDescription,
@@ -6,7 +7,7 @@ import {
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react";
import { CopyIcon, Loader2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
@@ -14,56 +15,66 @@ interface Props {
}
export const AddManager = ({ serverId }: Props) => {
const { data } = api.cluster.addManager.useQuery({ serverId });
const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({
serverId,
});
return (
<>
<div>
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
<DialogHeader>
<DialogTitle>Add a new manager</DialogTitle>
<DialogDescription>Add a new manager</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
<DialogHeader>
<DialogTitle>Add a new manager</DialogTitle>
<DialogDescription>Add a new manager</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{isLoading ? (
<Loader2 className="w-full animate-spin text-muted-foreground" />
) : (
<>
<div className="flex flex-col gap-2.5 text-sm">
<span>
1. Go to your new server and run the following command
</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<div className="flex flex-col gap-2.5 text-sm">
<span>
2. Run the following command to add the node(manager) to your
cluster
</span>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
</CardContent>
</div>
<div className="flex flex-col gap-2.5 text-sm">
<span>
2. Run the following command to add the node(manager) to your
cluster
</span>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
</>
)}
</CardContent>
</>
);
};

View File

@@ -17,7 +17,7 @@ export const ShowNodesModal = ({ serverId }: Props) => {
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Nodes
Show Swarm Nodes
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">

View File

@@ -35,9 +35,9 @@ import { api } from "@/utils/api";
import {
Boxes,
HelpCircle,
Loader2,
LockIcon,
MoreHorizontal,
Loader2,
} from "lucide-react";
import { toast } from "sonner";
import { AddNode } from "./add-node";
@@ -144,7 +144,7 @@ export const ShowNodes = ({ serverId }: Props) => {
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<ShowNodeData data={node} />
{node?.ManagerStatus?.Leader && (
{!node?.ManagerStatus?.Leader && (
<DialogAction
title="Delete Node"
description="Are you sure you want to delete this node from the cluster?"

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CardContent } from "@/components/ui/card";
import {
DialogDescription,
@@ -6,7 +7,7 @@ import {
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react";
import { CopyIcon, Loader2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
@@ -14,54 +15,62 @@ interface Props {
}
export const AddWorker = ({ serverId }: Props) => {
const { data } = api.cluster.addWorker.useQuery({ serverId });
const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({
serverId,
});
return (
<div>
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
<DialogHeader>
<DialogTitle>Add a new worker</DialogTitle>
<DialogDescription>Add a new worker</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
<DialogHeader>
<DialogTitle>Add a new worker</DialogTitle>
<DialogDescription>Add a new worker</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{isLoading ? (
<Loader2 className="w-full animate-spin text-muted-foreground" />
) : (
<>
<div className="flex flex-col gap-2.5 text-sm">
<span>1. Go to your new server and run the following command</span>
<span className="bg-muted rounded-lg p-2 flex justify-between">
curl https://get.docker.com | sh -s -- --version {data?.version}
<button
type="button"
className="self-center"
onClick={() => {
copy(
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
<div className="flex flex-col gap-2.5 text-sm">
<span>
2. Run the following command to add the node(worker) to your cluster
</span>
<div className="flex flex-col gap-2.5 text-sm">
<span>
2. Run the following command to add the node(worker) to your
cluster
</span>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
</CardContent>
</div>
<span className="bg-muted rounded-lg p-2 flex">
{data?.command}
<button
type="button"
className="self-start"
onClick={() => {
copy(data?.command || "");
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="h-4 w-4 cursor-pointer" />
</button>
</span>
</div>
</>
)}
</CardContent>
);
};

View File

@@ -0,0 +1,286 @@
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import {
type GiteaProviderResponse,
getGiteaOAuthUrl,
} from "@/utils/gitea-utils";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } 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";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
giteaUrl: z.string().min(1, {
message: "Gitea URL is required",
}),
clientId: z.string().min(1, {
message: "Client ID is required",
}),
clientSecret: z.string().min(1, {
message: "Client Secret is required",
}),
redirectUri: z.string().min(1, {
message: "Redirect URI is required",
}),
organizationName: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
export const AddGiteaProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const urlObj = useUrl();
const baseUrl =
typeof urlObj === "string" ? urlObj : (urlObj as any)?.url || "";
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
const form = useForm<Schema>({
defaultValues: {
clientId: "",
clientSecret: "",
redirectUri: webhookUrl,
name: "",
giteaUrl: "https://gitea.com",
},
resolver: zodResolver(Schema),
});
const giteaUrl = form.watch("giteaUrl");
useEffect(() => {
form.reset({
clientId: "",
clientSecret: "",
redirectUri: webhookUrl,
name: "",
giteaUrl: "https://gitea.com",
});
}, [form, webhookUrl, isOpen]);
const onSubmit = async (data: Schema) => {
try {
// Send the form data to create the Gitea provider
const result = (await mutateAsync({
clientId: data.clientId,
clientSecret: data.clientSecret,
name: data.name,
redirectUri: data.redirectUri,
giteaUrl: data.giteaUrl,
organizationName: data.organizationName,
})) as unknown as GiteaProviderResponse;
// Check if we have a giteaId from the response
if (!result || !result.giteaId) {
toast.error("Failed to get Gitea ID from response");
return;
}
// Generate OAuth URL using the shared utility
const authUrl = getGiteaOAuthUrl(
result.giteaId,
data.clientId,
data.giteaUrl,
baseUrl,
);
// Open the Gitea OAuth URL
if (authUrl !== "#") {
window.open(authUrl, "_blank");
} else {
toast.error("Configuration Incomplete", {
description: "Please fill in Client ID and Gitea URL first.",
});
}
toast.success("Gitea provider created successfully");
setIsOpen(false);
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(`Error configuring Gitea: ${error.message}`);
} else {
toast.error("An unknown error occurred.");
}
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="flex items-center space-x-1 bg-green-700 text-white hover:bg-green-500"
>
<GiteaIcon />
<span>Gitea</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Gitea Provider <GiteaIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-gitea"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
To integrate your Gitea account, you need to create a new
application in your Gitea settings. Follow these steps:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Go to your Gitea settings{" "}
<Link
href={`${giteaUrl}/user/settings/applications`}
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>
Navigate to Applications {"->"} Create new OAuth2
Application
</li>
<li>
Create a new application with the following details:
<ul className="list-disc list-inside ml-4">
<li>Name: Dokploy</li>
<li>
Redirect URI:{" "}
<span className="text-primary">{webhookUrl}</span>{" "}
</li>
</ul>
</li>
<li>
After creating, you'll receive an ID and Secret, copy them
and paste them below.
</li>
</ol>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="giteaUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Gitea URL</FormLabel>
<FormControl>
<Input placeholder="https://gitea.com/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="redirectUri"
render={({ field }) => (
<FormItem>
<FormLabel>Redirect URI</FormLabel>
<FormControl>
<Input
disabled
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>Client ID</FormLabel>
<FormControl>
<Input placeholder="Client ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Client Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Client Secret"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button isLoading={form.formState.isSubmitting}>
Configure Gitea App
</Button>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,296 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { getGiteaOAuthUrl } from "@/utils/gitea-utils";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
giteaUrl: z.string().min(1, "Gitea URL is required"),
clientId: z.string().min(1, "Client ID is required"),
clientSecret: z.string().min(1, "Client Secret is required"),
});
interface Props {
giteaId: string;
}
export const EditGiteaProvider = ({ giteaId }: Props) => {
const router = useRouter();
const [open, setOpen] = useState(false);
const {
data: gitea,
isLoading,
refetch,
} = api.gitea.one.useQuery({ giteaId });
const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
const { mutateAsync: testConnection, isLoading: isTesting } =
api.gitea.testConnection.useMutation();
const url = useUrl();
const utils = api.useUtils();
useEffect(() => {
const { connected, error } = router.query;
if (!router.isReady) return;
if (connected) {
toast.success("Successfully connected to Gitea", {
description: "Your Gitea provider has been authorized.",
id: "gitea-connection-success",
});
refetch();
router.replace(
{
pathname: router.pathname,
query: {},
},
undefined,
{ shallow: true },
);
}
if (error) {
toast.error("Gitea Connection Failed", {
description: decodeURIComponent(error as string),
id: "gitea-connection-error",
});
router.replace(
{
pathname: router.pathname,
query: {},
},
undefined,
{ shallow: true },
);
}
}, [router.query, router.isReady, refetch]);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
giteaUrl: "https://gitea.com",
clientId: "",
clientSecret: "",
},
});
useEffect(() => {
if (gitea) {
form.reset({
name: gitea.gitProvider?.name || "",
giteaUrl: gitea.giteaUrl || "https://gitea.com",
clientId: gitea.clientId || "",
clientSecret: gitea.clientSecret || "",
});
}
}, [gitea, form]);
const onSubmit = async (values: z.infer<typeof formSchema>) => {
await mutateAsync({
giteaId: giteaId,
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
name: values.name,
giteaUrl: values.giteaUrl,
clientId: values.clientId,
clientSecret: values.clientSecret,
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Gitea provider updated successfully");
await refetch();
setOpen(false);
})
.catch(() => {
toast.error("Error updating Gitea provider");
});
};
const handleTestConnection = async () => {
try {
const result = await testConnection({ giteaId });
toast.success("Gitea Connection Verified", {
description: result,
});
} catch (error: any) {
const formValues = form.getValues();
const authUrl =
error.authorizationUrl ||
getGiteaOAuthUrl(
giteaId,
formValues.clientId,
formValues.giteaUrl,
typeof url === "string" ? url : (url as any).url || "",
);
toast.error("Gitea Not Connected", {
description:
error.message || "Please complete the OAuth authorization process.",
action:
authUrl && authUrl !== "#"
? {
label: "Authorize Now",
onClick: () => window.open(authUrl, "_blank"),
}
: undefined,
});
}
};
if (isLoading) {
return (
<Button variant="ghost" size="icon" disabled>
<PenBoxIcon className="h-4 w-4 text-muted-foreground" />
</Button>
);
}
// Function to handle dialog open state
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10"
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Gitea Provider</DialogTitle>
<DialogDescription>
Update your Gitea provider details.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="My Gitea"
{...field}
autoFocus={false}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="giteaUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Gitea URL</FormLabel>
<FormControl>
<Input placeholder="https://gitea.example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>Client ID</FormLabel>
<FormControl>
<Input placeholder="Client ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Client Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Client Secret"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
isLoading={isTesting}
>
Test Connection
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
const formValues = form.getValues();
const authUrl = getGiteaOAuthUrl(
giteaId,
formValues.clientId,
formValues.giteaUrl,
typeof url === "string" ? url : (url as any).url || "",
);
if (authUrl !== "#") {
window.open(authUrl, "_blank");
}
}}
>
Connect to Gitea
</Button>
<Button type="submit" isLoading={isUpdating}>
Save
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,5 +1,6 @@
import {
BitbucketIcon,
GiteaIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
@@ -26,6 +27,8 @@ import Link from "next/link";
import { toast } from "sonner";
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
import { AddGiteaProvider } from "./gitea/add-gitea-provider";
import { EditGiteaProvider } from "./gitea/edit-gitea-provider";
import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
@@ -36,19 +39,18 @@ export const ShowGitProviders = () => {
const { mutateAsync, isLoading: isRemoving } =
api.gitProvider.remove.useMutation();
const url = useUrl();
const getGitlabUrl = (
clientId: string,
gitlabId: string,
gitlabUrl: string,
) => {
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
const scope = "api read_user read_repository";
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
return authUrl;
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
@@ -82,6 +84,7 @@ export const ShowGitProviders = () => {
<AddGithubProvider />
<AddGitlabProvider />
<AddBitbucketProvider />
<AddGiteaProvider />
</div>
</div>
</div>
@@ -97,6 +100,7 @@ export const ShowGitProviders = () => {
<AddGithubProvider />
<AddGitlabProvider />
<AddBitbucketProvider />
<AddGiteaProvider />
</div>
</div>
</div>
@@ -107,13 +111,16 @@ export const ShowGitProviders = () => {
const isGitlab = gitProvider.providerType === "gitlab";
const isBitbucket =
gitProvider.providerType === "bitbucket";
const isGitea = gitProvider.providerType === "gitea";
const haveGithubRequirements =
gitProvider.providerType === "github" &&
isGithub &&
gitProvider.github?.githubPrivateKey &&
gitProvider.github?.githubAppId &&
gitProvider.github?.githubInstallationId;
const haveGitlabRequirements =
isGitlab &&
gitProvider.gitlab?.accessToken &&
gitProvider.gitlab?.refreshToken;
@@ -122,18 +129,19 @@ export const ShowGitProviders = () => {
key={gitProvider.gitProviderId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex flex-col items-center justify-between">
<div className="flex gap-2 flex-row items-center">
{gitProvider.providerType === "github" && (
{isGithub && (
<GithubIcon className="size-5" />
)}
{gitProvider.providerType === "gitlab" && (
{isGitlab && (
<GitlabIcon className="size-5" />
)}
{gitProvider.providerType === "bitbucket" && (
{isBitbucket && (
<BitbucketIcon className="size-5" />
)}
{isGitea && <GiteaIcon className="size-5" />}
<div className="flex flex-col gap-1">
<span className="text-sm font-medium">
{gitProvider.name}
@@ -194,26 +202,33 @@ export const ShowGitProviders = () => {
</Link>
</div>
)}
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github.githubId}
githubId={gitProvider.github?.githubId}
/>
)}
{isGitlab && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab.gitlabId}
gitlabId={gitProvider.gitlab?.gitlabId}
/>
)}
{isBitbucket && (
<EditBitbucketProvider
bitbucketId={
gitProvider.bitbucket.bitbucketId
gitProvider.bitbucket?.bitbucketId
}
/>
)}
{isGitea && (
<EditGiteaProvider
giteaId={gitProvider.gitea?.giteaId}
/>
)}
<DialogAction
title="Delete Git Provider"
description="Are you sure you want to delete this Git Provider?"
@@ -238,7 +253,7 @@ export const ShowGitProviders = () => {
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />

View File

@@ -663,13 +663,16 @@ export const HandleNotifications = ({ notificationId }: Props) => {
{...field}
onChange={(e) => {
const value = e.target.value;
if (value) {
if (value === "") {
field.onChange(undefined);
} else {
const port = Number.parseInt(value);
if (port > 0 && port < 65536) {
field.onChange(port);
}
}
}}
value={field.value || ""}
type="number"
/>
</FormControl>

View File

@@ -25,7 +25,7 @@ export const SecurityAudit = ({ serverId }: Props) => {
enabled: !!serverId,
},
);
const _utils = api.useUtils();
return (
<CardContent className="p-0">
<div className="flex flex-col gap-4">
@@ -36,10 +36,12 @@ export const SecurityAudit = ({ serverId }: Props) => {
<div className="flex items-center gap-2">
<LockKeyhole className="size-5" />
<CardTitle className="text-xl">
Setup Security Sugestions
Setup Security Suggestions
</CardTitle>
</div>
<CardDescription>Check the security sugestions</CardDescription>
<CardDescription>
Check the security suggestions
</CardDescription>
</div>
<Button
isLoading={isRefreshing}
@@ -120,36 +122,36 @@ export const SecurityAudit = ({ serverId }: Props) => {
<div className="grid gap-2.5">
<StatusRow
label="Enabled"
isEnabled={data?.ssh.enabled}
isEnabled={data?.ssh?.enabled}
description={
data?.ssh.enabled
data?.ssh?.enabled
? "Enabled"
: "Not Enabled (SSH should be enabled)"
}
/>
<StatusRow
label="Key Auth"
isEnabled={data?.ssh.keyAuth}
isEnabled={data?.ssh?.keyAuth}
description={
data?.ssh.keyAuth
data?.ssh?.keyAuth
? "Enabled (Recommended)"
: "Not Enabled (Key Authentication should be enabled)"
}
/>
<StatusRow
label="Password Auth"
isEnabled={data?.ssh.passwordAuth === "no"}
isEnabled={data?.ssh?.passwordAuth === "no"}
description={
data?.ssh.passwordAuth === "no"
data?.ssh?.passwordAuth === "no"
? "Disabled (Recommended)"
: "Enabled (Password Authentication should be disabled)"
}
/>
<StatusRow
label="Use PAM"
isEnabled={data?.ssh.usePam === "no"}
isEnabled={data?.ssh?.usePam === "no"}
description={
data?.ssh.usePam === "no"
data?.ssh?.usePam === "no"
? "Disabled (Recommended for key-based auth)"
: "Enabled (Should be disabled when using key-based auth)"
}
@@ -166,9 +168,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
<div className="grid gap-2.5">
<StatusRow
label="Installed"
isEnabled={data?.fail2ban.installed}
isEnabled={data?.fail2ban?.installed}
description={
data?.fail2ban.installed
data?.fail2ban?.installed
? "Installed (Recommended)"
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
}
@@ -176,18 +178,18 @@ export const SecurityAudit = ({ serverId }: Props) => {
<StatusRow
label="Enabled"
isEnabled={data?.fail2ban.enabled}
isEnabled={data?.fail2ban?.enabled}
description={
data?.fail2ban.enabled
data?.fail2ban?.enabled
? "Enabled (Recommended)"
: "Not Enabled (Fail2Ban service should be enabled)"
}
/>
<StatusRow
label="Active"
isEnabled={data?.fail2ban.active}
isEnabled={data?.fail2ban?.active}
description={
data?.fail2ban.active
data?.fail2ban?.active
? "Active (Recommended)"
: "Not Active (Fail2Ban service should be running)"
}
@@ -195,9 +197,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
<StatusRow
label="SSH Protection"
isEnabled={data?.fail2ban.sshEnabled === "true"}
isEnabled={data?.fail2ban?.sshEnabled === "true"}
description={
data?.fail2ban.sshEnabled === "true"
data?.fail2ban?.sshEnabled === "true"
? "Enabled (Recommended)"
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
}
@@ -205,11 +207,11 @@ export const SecurityAudit = ({ serverId }: Props) => {
<StatusRow
label="SSH Mode"
isEnabled={data?.fail2ban.sshMode === "aggressive"}
isEnabled={data?.fail2ban?.sshMode === "aggressive"}
description={
data?.fail2ban.sshMode === "aggressive"
data?.fail2ban?.sshMode === "aggressive"
? "Aggressive Mode (Recommended)"
: `Mode: ${data?.fail2ban.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
: `Mode: ${data?.fail2ban?.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
}
/>
</div>

View File

@@ -33,6 +33,7 @@ import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions";
import { HandleServers } from "./handle-servers";
@@ -42,7 +43,6 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
export const ShowServers = () => {
const { t } = useTranslation("settings");

View File

@@ -112,15 +112,17 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
toast.error("Error generating the SSH Key");
});
const downloadKey = (
content: string,
defaultFilename: string,
keyType: "private" | "public",
) => {
const downloadKey = (content: string, keyType: "private" | "public") => {
const keyName = form.watch("name");
const publicKey = form.watch("publicKey");
// Extract algorithm type from public key
const isEd25519 = publicKey.startsWith("ssh-ed25519");
const defaultName = isEd25519 ? "id_ed25519" : "id_rsa";
const filename = keyName
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultFilename}`
: `${keyType}_${defaultFilename}`;
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultName}${keyType === "public" ? ".pub" : ""}`
: `${defaultName}${keyType === "public" ? ".pub" : ""}`;
const blob = new Blob([content], { type: "text/plain" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
@@ -273,7 +275,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
variant="outline"
size="default"
onClick={() =>
downloadKey(form.watch("privateKey"), "id_rsa", "private")
downloadKey(form.watch("privateKey"), "private")
}
className="flex items-center gap-2"
>
@@ -287,11 +289,7 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
variant="outline"
size="default"
onClick={() =>
downloadKey(
form.watch("publicKey"),
"id_rsa.pub",
"public",
)
downloadKey(form.watch("publicKey"), "public")
}
className="flex items-center gap-2"
>

View File

@@ -159,9 +159,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
onChange={(e) => {
const value = e.target.value;
field.onChange(
value === ""
? undefined
: Number(value),
);
}}
value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 8080"
/>
@@ -185,9 +191,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
onChange={(e) => {
const value = e.target.value;
field.onChange(
value === ""
? undefined
: Number(value),
);
}}
value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 80"
/>

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
@@ -22,7 +23,6 @@ import dynamic from "next/dynamic";
import type React from "react";
import { useEffect, useState } from "react";
import { badgeStateColor } from "../../application/logs/show";
import { Badge } from "@/components/ui/badge";
export const DockerLogsId = dynamic(
() =>

View File

@@ -12,7 +12,7 @@ import {
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { DatabaseIcon, AlertTriangle } from "lucide-react";
import { AlertTriangle, DatabaseIcon } from "lucide-react";
import { toast } from "sonner";
interface Props {

View File

@@ -1,6 +1,6 @@
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { RebuildDatabase } from "./rebuild-database";
interface Props {

View File

@@ -238,6 +238,41 @@ export const BitbucketIcon = ({ className }: Props) => {
);
};
export const GiteaIcon = ({ className }: Props) => {
return (
<svg
className={className}
version="1.1"
id="main_outline"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="5.67 143.05 628.65 387.55"
enableBackground="new 0 0 640 640"
>
<g>
<path
id="teabag"
style={{ fill: "#FFFFFF" }}
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"
/>
<g>
<g>
<path
style={{ fill: "#609926" }}
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"
/>
<path
style={{ fill: "#609926" }}
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"
/>
</g>
</g>
</g>
</svg>
);
};
export const DockerIcon = ({ className }: Props) => {
return (
<svg

View File

@@ -1,10 +1,10 @@
import { api } from "@/utils/api";
import type { IUpdateData } from "@dokploy/server/index";
import { Download } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import UpdateServer from "../dashboard/settings/web-server/update-server";
import { Button } from "../ui/button";
import { Download } from "lucide-react";
import {
Tooltip,
TooltipContent,

View File

@@ -37,7 +37,9 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
)}
</BreadcrumbLink>
</BreadcrumbItem>
{_index + 1 < list.length && <BreadcrumbSeparator className="block" />}
{_index + 1 < list.length && (
<BreadcrumbSeparator className="block" />
)}
</Fragment>
))}
</BreadcrumbList>

View File

@@ -3,18 +3,18 @@ import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import {
type Completion,
type CompletionContext,
type CompletionResult,
autocompletion,
} from "@codemirror/autocomplete";
import { properties } from "@codemirror/legacy-modes/mode/properties";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { EditorView } from "@codemirror/view";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { useTheme } from "next-themes";
import {
autocompletion,
type CompletionContext,
type CompletionResult,
type Completion,
} from "@codemirror/autocomplete";
// Docker Compose completion options
const dockerComposeServices = [

View File

@@ -1,9 +1,9 @@
import type * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import type * as React from "react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;

View File

@@ -0,0 +1,31 @@
ALTER TYPE "public"."sourceType" ADD VALUE 'gitea' BEFORE 'drop';--> statement-breakpoint
ALTER TYPE "public"."sourceTypeCompose" ADD VALUE 'gitea' BEFORE 'raw';--> statement-breakpoint
ALTER TYPE "public"."gitProviderType" ADD VALUE 'gitea';--> statement-breakpoint
CREATE TABLE "gitea" (
"giteaId" text PRIMARY KEY NOT NULL,
"giteaUrl" text DEFAULT 'https://gitea.com' NOT NULL,
"redirect_uri" text,
"client_id" text,
"client_secret" text,
"gitProviderId" text NOT NULL,
"gitea_username" text,
"access_token" text,
"refresh_token" text,
"expires_at" integer,
"scopes" text DEFAULT 'repo,repo:status,read:user,read:org',
"last_authenticated_at" integer
);
--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaProjectId" integer;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaBuildPath" text DEFAULT '/';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaId" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaId" text;--> statement-breakpoint
ALTER TABLE "gitea" ADD CONSTRAINT "gitea_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "application" ADD CONSTRAINT "application_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "compose" ADD CONSTRAINT "compose_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" DROP COLUMN "giteaProjectId";

View File

@@ -0,0 +1 @@
ALTER TABLE "gitea" DROP COLUMN "gitea_username";

View File

@@ -0,0 +1,2 @@
ALTER TABLE "backup" ADD COLUMN "userId" text;--> statement-breakpoint
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -0,0 +1 @@
ALTER TYPE "public"."databaseType" ADD VALUE 'web-server';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -554,6 +554,41 @@
"when": 1742112194375,
"tag": "0078_uneven_omega_sentinel",
"breakpoints": true
},
{
"idx": 79,
"version": "7",
"when": 1742281690186,
"tag": "0079_bizarre_wendell_rand",
"breakpoints": true
},
{
"idx": 80,
"version": "7",
"when": 1743280866402,
"tag": "0080_sleepy_sinister_six",
"breakpoints": true
},
{
"idx": 81,
"version": "7",
"when": 1743281254393,
"tag": "0081_lovely_mentallo",
"breakpoints": true
},
{
"idx": 82,
"version": "7",
"when": 1743287689974,
"tag": "0082_clean_mandarin",
"breakpoints": true
},
{
"idx": 83,
"version": "7",
"when": 1743288371413,
"tag": "0083_parallel_stranger",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,552 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1713262741218,
"tag": "0000_reflective_puck",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1713761637676,
"tag": "0001_striped_tattoo",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1713763492341,
"tag": "0002_ambiguous_carlie_cooper",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1713947141424,
"tag": "0003_square_lightspeed",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1714004732716,
"tag": "0004_nice_tenebrous",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1715551130605,
"tag": "0005_cute_terror",
"breakpoints": true
},
{
"idx": 6,
"version": "6",
"when": 1715563165991,
"tag": "0006_oval_jimmy_woo",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1715563497100,
"tag": "0007_cute_guardsmen",
"breakpoints": true
},
{
"idx": 8,
"version": "6",
"when": 1715564143641,
"tag": "0008_lazy_sage",
"breakpoints": true
},
{
"idx": 9,
"version": "6",
"when": 1715564774423,
"tag": "0009_majestic_spencer_smythe",
"breakpoints": true
},
{
"idx": 10,
"version": "6",
"when": 1715574037832,
"tag": "0010_lean_black_widow",
"breakpoints": true
},
{
"idx": 11,
"version": "6",
"when": 1715574230599,
"tag": "0011_petite_calypso",
"breakpoints": true
},
{
"idx": 12,
"version": "6",
"when": 1716015716708,
"tag": "0012_chubby_umar",
"breakpoints": true
},
{
"idx": 13,
"version": "6",
"when": 1716076179443,
"tag": "0013_blushing_starjammers",
"breakpoints": true
},
{
"idx": 14,
"version": "6",
"when": 1716715367982,
"tag": "0014_same_hammerhead",
"breakpoints": true
},
{
"idx": 15,
"version": "6",
"when": 1717564517104,
"tag": "0015_fearless_callisto",
"breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1719109196484,
"tag": "0016_chunky_leopardon",
"breakpoints": true
},
{
"idx": 17,
"version": "6",
"when": 1719547174326,
"tag": "0017_minor_post",
"breakpoints": true
},
{
"idx": 18,
"version": "6",
"when": 1719928377858,
"tag": "0018_careful_killmonger",
"breakpoints": true
},
{
"idx": 19,
"version": "6",
"when": 1721110706912,
"tag": "0019_heavy_freak",
"breakpoints": true
},
{
"idx": 20,
"version": "6",
"when": 1721363861686,
"tag": "0020_fantastic_slapstick",
"breakpoints": true
},
{
"idx": 21,
"version": "6",
"when": 1721370423752,
"tag": "0021_premium_sebastian_shaw",
"breakpoints": true
},
{
"idx": 22,
"version": "6",
"when": 1721531163852,
"tag": "0022_warm_colonel_america",
"breakpoints": true
},
{
"idx": 23,
"version": "6",
"when": 1721542782659,
"tag": "0023_icy_maverick",
"breakpoints": true
},
{
"idx": 24,
"version": "6",
"when": 1721603595092,
"tag": "0024_dapper_supernaut",
"breakpoints": true
},
{
"idx": 25,
"version": "6",
"when": 1721633853118,
"tag": "0025_lying_mephisto",
"breakpoints": true
},
{
"idx": 26,
"version": "6",
"when": 1721979220929,
"tag": "0026_known_dormammu",
"breakpoints": true
},
{
"idx": 27,
"version": "6",
"when": 1722445099203,
"tag": "0027_red_lady_bullseye",
"breakpoints": true
},
{
"idx": 28,
"version": "6",
"when": 1722503439951,
"tag": "0028_jittery_eternity",
"breakpoints": true
},
{
"idx": 29,
"version": "6",
"when": 1722578386823,
"tag": "0029_colossal_zodiak",
"breakpoints": true
},
{
"idx": 30,
"version": "6",
"when": 1723608499147,
"tag": "0030_little_kabuki",
"breakpoints": true
},
{
"idx": 31,
"version": "6",
"when": 1723701656243,
"tag": "0031_steep_vulture",
"breakpoints": true
},
{
"idx": 32,
"version": "6",
"when": 1723705257806,
"tag": "0032_flashy_shadow_king",
"breakpoints": true
},
{
"idx": 33,
"version": "6",
"when": 1725250322137,
"tag": "0033_white_hawkeye",
"breakpoints": true
},
{
"idx": 34,
"version": "6",
"when": 1725256397019,
"tag": "0034_aspiring_secret_warriors",
"breakpoints": true
},
{
"idx": 35,
"version": "6",
"when": 1725429324584,
"tag": "0035_cool_gravity",
"breakpoints": true
},
{
"idx": 36,
"version": "6",
"when": 1725519351871,
"tag": "0036_tired_ronan",
"breakpoints": true
},
{
"idx": 37,
"version": "6",
"when": 1726988289562,
"tag": "0037_legal_namor",
"breakpoints": true
},
{
"idx": 38,
"version": "6",
"when": 1727942090102,
"tag": "0038_rapid_landau",
"breakpoints": true
},
{
"idx": 39,
"version": "6",
"when": 1728021127765,
"tag": "0039_many_tiger_shark",
"breakpoints": true
},
{
"idx": 40,
"version": "6",
"when": 1728780577084,
"tag": "0040_graceful_wolfsbane",
"breakpoints": true
},
{
"idx": 41,
"version": "6",
"when": 1729667438853,
"tag": "0041_huge_bruce_banner",
"breakpoints": true
},
{
"idx": 42,
"version": "6",
"when": 1729984439862,
"tag": "0042_fancy_havok",
"breakpoints": true
},
{
"idx": 43,
"version": "6",
"when": 1731873965888,
"tag": "0043_closed_naoko",
"breakpoints": true
},
{
"idx": 44,
"version": "6",
"when": 1731875539532,
"tag": "0044_sour_true_believers",
"breakpoints": true
},
{
"idx": 45,
"version": "6",
"when": 1732644181718,
"tag": "0045_smiling_blur",
"breakpoints": true
},
{
"idx": 46,
"version": "6",
"when": 1732851191048,
"tag": "0046_purple_sleeper",
"breakpoints": true
},
{
"idx": 47,
"version": "6",
"when": 1733599090582,
"tag": "0047_tidy_revanche",
"breakpoints": true
},
{
"idx": 48,
"version": "6",
"when": 1733599163710,
"tag": "0048_flat_expediter",
"breakpoints": true
},
{
"idx": 49,
"version": "6",
"when": 1733628762978,
"tag": "0049_dark_leopardon",
"breakpoints": true
},
{
"idx": 50,
"version": "6",
"when": 1733889104203,
"tag": "0050_nappy_wrecker",
"breakpoints": true
},
{
"idx": 51,
"version": "6",
"when": 1734241482851,
"tag": "0051_hard_gorgon",
"breakpoints": true
},
{
"idx": 52,
"version": "6",
"when": 1734809337308,
"tag": "0052_bumpy_luckman",
"breakpoints": true
},
{
"idx": 53,
"version": "6",
"when": 1735118844878,
"tag": "0053_broken_kulan_gath",
"breakpoints": true
},
{
"idx": 54,
"version": "6",
"when": 1736669421560,
"tag": "0054_nervous_spencer_smythe",
"breakpoints": true
},
{
"idx": 55,
"version": "6",
"when": 1736669623831,
"tag": "0055_next_serpent_society",
"breakpoints": true
},
{
"idx": 56,
"version": "6",
"when": 1736789918294,
"tag": "0056_majestic_skaar",
"breakpoints": true
},
{
"idx": 57,
"version": "6",
"when": 1737306063563,
"tag": "0057_tricky_living_tribunal",
"breakpoints": true
},
{
"idx": 58,
"version": "6",
"when": 1737612903012,
"tag": "0058_brown_sharon_carter",
"breakpoints": true
},
{
"idx": 59,
"version": "6",
"when": 1737615160768,
"tag": "0059_striped_bill_hollister",
"breakpoints": true
},
{
"idx": 60,
"version": "6",
"when": 1737929896838,
"tag": "0060_disable-aggressive-cache",
"breakpoints": true
},
{
"idx": 61,
"version": "7",
"when": 1738481304953,
"tag": "0061_many_molten_man",
"breakpoints": true
},
{
"idx": 62,
"version": "7",
"when": 1738482795112,
"tag": "0062_slippery_white_tiger",
"breakpoints": true
},
{
"idx": 63,
"version": "7",
"when": 1738522845992,
"tag": "0063_panoramic_dreadnoughts",
"breakpoints": true
},
{
"idx": 64,
"version": "7",
"when": 1738564387043,
"tag": "0064_previous_agent_brand",
"breakpoints": true
},
{
"idx": 65,
"version": "7",
"when": 1739087857244,
"tag": "0065_daily_zaladane",
"breakpoints": true
},
{
"idx": 66,
"version": "7",
"when": 1739426913392,
"tag": "0066_yielding_echo",
"breakpoints": true
},
{
"idx": 67,
"version": "7",
"when": 1740892043121,
"tag": "0067_condemned_sugar_man",
"breakpoints": true
},
{
"idx": 68,
"version": "7",
"when": 1740897756774,
"tag": "0068_complex_rhino",
"breakpoints": true
},
{
"idx": 69,
"version": "7",
"when": 1741152916611,
"tag": "0069_legal_bill_hollister",
"breakpoints": true
},
{
"idx": 70,
"version": "7",
"when": 1741322697251,
"tag": "0070_useful_serpent_society",
"breakpoints": true
},
{
"idx": 71,
"version": "7",
"when": 1741559743256,
"tag": "0071_flimsy_plazm",
"breakpoints": true
},
{
"idx": 72,
"version": "7",
"when": 1741593124105,
"tag": "0072_low_redwing",
"breakpoints": true
},
{
"idx": 73,
"version": "7",
"when": 1741645208694,
"tag": "0073_dark_tigra",
"breakpoints": true
},
{
"idx": 74,
"version": "7",
"when": 1741673569715,
"tag": "0074_military_miss_america",
"breakpoints": true
},
{
"idx": 75,
"version": "7",
"when": 1742018928109,
"tag": "0075_wild_xorn",
"breakpoints": true
},
{
"idx": 76,
"version": "7",
"when": 1742237840762,
"tag": "0076_tough_iron_patriot",
"breakpoints": true
},
{
"idx": 77,
"version": "7",
"when": 1742238314349,
"tag": "0077_mature_tomorrow_man",
"breakpoints": true
}
]
}

View File

@@ -1,32 +0,0 @@
// middleware.ts
import { verifyRequestOrigin } from "lucia";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export async function middleware(request: NextRequest): Promise<NextResponse> {
if (request.method === "GET") {
return NextResponse.next();
}
const originHeader = request.headers.get("Origin");
const hostHeader = request.headers.get("Host");
if (
!originHeader ||
!hostHeader ||
!verifyRequestOrigin(originHeader, [hostHeader])
) {
return new NextResponse(null, {
status: 403,
});
}
return NextResponse.next();
}
export const config = {
matcher: [
// Don't handle HMR requests for the dev server we rewrite to
"/settings",
"/dashboard/(.*)",
"/invitation",
],
};

View File

@@ -5,23 +5,23 @@
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
transpilePackages: ["@dokploy/server"],
/**
* If you are using `appDir` then you must comment the below `i18n` config out.
*
* @see https://github.com/vercel/next.js/issues/41980
*/
i18n: {
locales: ["en"],
defaultLocale: "en",
},
reactStrictMode: true,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
transpilePackages: ["@dokploy/server"],
/**
* If you are using `appDir` then you must comment the below `i18n` config out.
*
* @see https://github.com/vercel/next.js/issues/41980
*/
i18n: {
locales: ["en"],
defaultLocale: "en",
},
};
export default nextConfig;

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.20.4",
"version": "v0.21.2",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -53,7 +53,6 @@
"@dokploy/trpc-openapi": "0.0.4",
"@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^3.9.0",
"@lucia-auth/adapter-drizzle": "1.0.7",
"@octokit/auth-app": "^6.0.4",
"@octokit/webhooks": "^13.2.7",
"@radix-ui/react-accordion": "1.1.2",
@@ -113,11 +112,10 @@
"js-cookie": "^3.0.5",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"lucia": "^3.0.1",
"lucide-react": "^0.469.0",
"micromatch": "4.0.8",
"nanoid": "3",
"next": "^15.0.1",
"next": "^15.2.4",
"next-i18next": "^15.3.1",
"next-themes": "^0.2.1",
"node-os-utils": "1.3.7",
@@ -152,7 +150,8 @@
"ws": "8.16.0",
"xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4",
"zod-form-data": "^2.0.2"
"zod-form-data": "^2.0.2",
"toml": "3.0.0"
},
"devDependencies": {
"@types/adm-zip": "^0.5.5",

View File

@@ -84,6 +84,33 @@ export default async function handler(
res.status(301).json({ message: "Branch Not Match" });
return;
}
const provider = getProviderByHeader(req.headers);
let normalizedCommits: string[] = [];
if (provider === "github") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
} else if (provider === "gitlab") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
} else if (provider === "gitea") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
}
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
@@ -128,6 +155,27 @@ export default async function handler(
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
} else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
if (!branchName || branchName !== application.giteaBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
}
try {
@@ -280,6 +328,26 @@ export const extractBranchName = (headers: any, body: any) => {
return null;
};
export const getProviderByHeader = (headers: any) => {
if (headers["x-github-event"]) {
return "github";
}
if (headers["x-gitea-event"]) {
return "gitea";
}
if (headers["x-gitlab-event"]) {
return "gitlab";
}
if (headers["x-event-key"]?.includes("repo:push")) {
return "bitbucket";
}
return null;
};
export const extractCommitedPaths = async (
body: any,
bitbucketUsername: string | null,

View File

@@ -8,9 +8,10 @@ import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import {
extractBranchName,
extractCommitedPaths,
extractCommitMessage,
extractCommitedPaths,
extractHash,
getProviderByHeader,
} from "../[refreshToken]";
export default async function handler(
@@ -91,12 +92,6 @@ export default async function handler(
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "git") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== composeResult.customGitBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
const commitedPaths = await extractCommitedPaths(
req.body,
@@ -104,6 +99,7 @@ export default async function handler(
composeResult.bitbucket?.appPassword || "",
composeResult.bitbucketRepository || "",
);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
commitedPaths,
@@ -113,6 +109,59 @@ export default async function handler(
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
} else if (sourceType === "git") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== composeResult.customGitBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
const provider = getProviderByHeader(req.headers);
let normalizedCommits: string[] = [];
if (provider === "github") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
} else if (provider === "gitlab") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
} else if (provider === "gitea") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
}
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
} else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
if (!branchName || branchName !== composeResult.giteaBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
}
try {

View File

@@ -93,6 +93,7 @@ export default async function handler(
try {
const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = githubBody?.repository?.owner?.name;
@@ -107,6 +108,7 @@ export default async function handler(
eq(applications.branch, branchName),
eq(applications.repository, repository),
eq(applications.owner, owner),
eq(applications.githubId, githubResult.githubId),
),
});
@@ -151,6 +153,7 @@ export default async function handler(
eq(compose.branch, branchName),
eq(compose.repository, repository),
eq(compose.owner, owner),
eq(compose.githubId, githubResult.githubId),
),
});
@@ -240,6 +243,7 @@ export default async function handler(
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
eq(applications.githubId, githubResult.githubId),
),
with: {
previewDeployments: true,

View File

@@ -0,0 +1,41 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { findGitea, redirectWithError } from "./helper";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
if (req.method !== "GET") {
return res.status(405).json({ error: "Method not allowed" });
}
const { giteaId } = req.query;
if (!giteaId || Array.isArray(giteaId)) {
return res.status(400).json({ error: "Invalid Gitea provider ID" });
}
const gitea = await findGitea(giteaId as string);
if (!gitea || !gitea.clientId || !gitea.redirectUri) {
return redirectWithError(res, "Incomplete OAuth configuration");
}
// Generate the Gitea authorization URL
const authorizationUrl = new URL(`${gitea.giteaUrl}/login/oauth/authorize`);
authorizationUrl.searchParams.append("client_id", gitea.clientId as string);
authorizationUrl.searchParams.append("response_type", "code");
authorizationUrl.searchParams.append(
"redirect_uri",
gitea.redirectUri as string,
);
authorizationUrl.searchParams.append("scope", "read:user repo");
authorizationUrl.searchParams.append("state", giteaId as string);
// Redirect user to Gitea authorization URL
return res.redirect(307, authorizationUrl.toString());
} catch (error) {
console.error("Error initiating Gitea OAuth flow:", error);
return res.status(500).json({ error: "Internal server error" });
}
}

View File

@@ -0,0 +1,93 @@
import { updateGitea } from "@dokploy/server";
import type { NextApiRequest, NextApiResponse } from "next";
import { type Gitea, findGitea, redirectWithError } from "./helper";
// Helper to parse the state parameter
const parseState = (state: string): string | null => {
try {
const stateObj =
state.startsWith("{") && state.endsWith("}") ? JSON.parse(state) : {};
return stateObj.giteaId || state || null;
} catch {
return null;
}
};
// Helper to fetch access token from Gitea
const fetchAccessToken = async (gitea: Gitea, code: string) => {
const response = await fetch(`${gitea.giteaUrl}/login/oauth/access_token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
client_id: gitea.clientId as string,
client_secret: gitea.clientSecret as string,
code,
grant_type: "authorization_code",
redirect_uri: gitea.redirectUri || "",
}),
});
const responseText = await response.text();
return response.ok
? JSON.parse(responseText)
: { error: "Token exchange failed", responseText };
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { code, state } = req.query;
if (!code || Array.isArray(code) || !state || Array.isArray(state)) {
return redirectWithError(
res,
"Invalid authorization code or state parameter",
);
}
const giteaId = parseState(state as string);
if (!giteaId) return redirectWithError(res, "Invalid state format");
const gitea = await findGitea(giteaId);
if (!gitea) return redirectWithError(res, "Failed to find Gitea provider");
// Fetch the access token from Gitea
const result = await fetchAccessToken(gitea, code as string);
if (result.error) {
console.error("Token exchange failed:", result);
return redirectWithError(res, result.error);
}
if (!result.access_token) {
console.error("Missing access token:", result);
return redirectWithError(res, "No access token received");
}
const expiresAt = result.expires_in
? Math.floor(Date.now() / 1000) + result.expires_in
: null;
try {
await updateGitea(gitea.giteaId, {
accessToken: result.access_token,
refreshToken: result.refresh_token,
expiresAt,
...(result.organizationName
? { organizationName: result.organizationName }
: {}),
});
return res.redirect(
307,
"/dashboard/settings/git-providers?connected=true",
);
} catch (updateError) {
console.error("Failed to update Gitea provider:", updateError);
return redirectWithError(res, "Failed to store access token");
}
}

View File

@@ -0,0 +1,39 @@
import { findGiteaById } from "@dokploy/server";
import type { NextApiResponse } from "next";
export interface Gitea {
giteaId: string;
gitProviderId: string;
redirectUri: string | null;
accessToken: string | null;
refreshToken: string | null;
expiresAt: number | null;
giteaUrl: string;
clientId: string | null;
clientSecret: string | null;
organizationName?: string;
gitProvider: {
name: string;
gitProviderId: string;
providerType: "github" | "gitlab" | "bitbucket" | "gitea";
createdAt: string;
organizationId: string;
};
}
export const findGitea = async (giteaId: string): Promise<Gitea | null> => {
try {
const gitea = await findGiteaById(giteaId);
return gitea;
} catch (findError) {
console.error("Error finding Gitea provider:", findError);
return null;
}
};
export const redirectWithError = (res: NextApiResponse, error: string) => {
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
);
};

View File

@@ -34,6 +34,15 @@ import {
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -47,6 +56,13 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
@@ -64,8 +80,8 @@ import {
Loader2,
PlusIcon,
Search,
X,
Trash2,
X,
} from "lucide-react";
import type {
GetServerSidePropsContext,
@@ -73,25 +89,10 @@ import type {
} from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { type ReactElement, useMemo, useState, useEffect } from "react";
import { type ReactElement, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
export type Services = {
appName: string;
@@ -553,7 +554,7 @@ const Project = (
</CardTitle>
<CardDescription>{data?.description}</CardDescription>
</CardHeader>
{(auth?.role === "owner" || auth?.canCreateServices) && (
<div className="flex flex-row gap-4 flex-wrap justify-between items-center">
<div className="flex flex-row gap-4 flex-wrap">
<ProjectEnvironment projectId={projectId}>
<Button variant="outline">Project Environment</Button>
@@ -569,7 +570,7 @@ const Project = (
className="w-[200px] space-y-2"
align="end"
>
<DropdownMenuLabel className="text-sm font-normal ">
<DropdownMenuLabel className="text-sm font-normal">
Actions
</DropdownMenuLabel>
<DropdownMenuSeparator />
@@ -593,7 +594,7 @@ const Project = (
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
</div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
{isLoading ? (
@@ -670,20 +671,27 @@ const Project = (
</DialogAction>
{(auth?.role === "owner" ||
auth?.canDeleteServices) && (
<DialogAction
title="Delete Services"
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
type="destructive"
onClick={handleBulkDelete}
>
<Button
variant="ghost"
className="w-full justify-start text-destructive"
<>
<DialogAction
title="Delete Services"
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
type="destructive"
onClick={handleBulkDelete}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
</DialogAction>
<Button
variant="ghost"
className="w-full justify-start text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
</DialogAction>
<DuplicateProject
projectId={projectId}
services={applications}
selectedServiceIds={selectedServices}
/>
</>
)}
<Dialog

View File

@@ -1,3 +1,4 @@
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
@@ -47,7 +48,6 @@ import { useRouter } from "next/router";
import { type ReactElement, useEffect, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
type TabState =
| "projects"

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